From deee2ac0b17e85672c68587a14bb025ae4fc5f70 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 12:20:16 -0800 Subject: [PATCH 01/89] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8bbc140..da46ac3 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.rundeck rundeck-api-java-client - 9.1 + 9.2-SNAPSHOT jar RunDeck API - Java Client Java client for the RunDeck REST API From b08edf901029eb2b36b899bde8f710ddd50c689e Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 23 Jan 2014 12:08:40 -0800 Subject: [PATCH 02/89] ApiVersion cannot be set to something less than 1 --- src/main/java/org/rundeck/api/RundeckClient.java | 6 +++--- src/test/java/org/rundeck/api/RundeckClientTest.java | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 4edd396..b218702 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -157,15 +157,15 @@ public class RundeckClient implements Serializable { } int getApiVersion() { - return apiVersion; + return (apiVersion > 0 ? apiVersion : API_VERSION); } void setApiVersion(int apiVersion) { - this.apiVersion = apiVersion; + this.apiVersion = (apiVersion > 0 ? apiVersion : API_VERSION); } void setApiVersion(Version apiVersion) { - this.apiVersion = apiVersion.getVersionNumber(); + setApiVersion(apiVersion.getVersionNumber()); } String getApiEndpoint() { diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index 6865f1a..c5330be 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -61,6 +61,18 @@ public class RundeckClientTest { private RundeckClient client; + @Test + public void apiVersionDefaultLatest() { + RundeckClient blah = createClient("blah", 0); + Assert.assertEquals("/api/" + RundeckClient.API_VERSION, blah.getApiEndpoint()); + Assert.assertEquals(RundeckClient.API_VERSION, blah.getApiVersion()); + blah.setApiVersion(0); + Assert.assertEquals(RundeckClient.API_VERSION, blah.getApiVersion()); + blah.setApiVersion(-1); + Assert.assertEquals(RundeckClient.API_VERSION, blah.getApiVersion()); + blah.setApiVersion(RundeckClient.Version.V9.getVersionNumber()); + Assert.assertEquals(RundeckClient.Version.V9.getVersionNumber(), blah.getApiVersion()); + } @Test @Betamax(tape = "get_projects") public void getProjects() throws Exception { From aee7b2edac68481bc29bb95ea8022de2f0a910d1 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 23 Jan 2014 12:09:52 -0800 Subject: [PATCH 03/89] Add script to run maven with more memory --- mvn.sh | 3 +++ 1 file changed, 3 insertions(+) create mode 100755 mvn.sh diff --git a/mvn.sh b/mvn.sh new file mode 100755 index 0000000..7d7afa6 --- /dev/null +++ b/mvn.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +MAVEN_OPTS="-Xmx512m -XX:MaxPermSize=192m" exec mvn "$@" From aa60a9fc56d058eaad9b1de1b38577198292e384 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 23 Jan 2014 12:26:04 -0800 Subject: [PATCH 04/89] [maven-release-plugin] prepare release rundeck-api-java-client-9.2 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index da46ac3..bea755f 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.rundeck rundeck-api-java-client - 9.2-SNAPSHOT + 9.2 jar RunDeck API - Java Client Java client for the RunDeck REST API From b554f9bc732479049f893ed037277fe5f81756ab Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 23 Jan 2014 12:26:08 -0800 Subject: [PATCH 05/89] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bea755f..b0857ca 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.rundeck rundeck-api-java-client - 9.2 + 9.3-SNAPSHOT jar RunDeck API - Java Client Java client for the RunDeck REST API From 3cf156dea8cae78602841cff5b18419850a5f3b0 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Fri, 24 Jan 2014 15:36:19 -0800 Subject: [PATCH 06/89] Handle login with Tomcat correctly fixes #7 thanks @katanafleet --- src/main/java/org/rundeck/api/ApiCall.java | 129 ++++++++++++--------- 1 file changed, 76 insertions(+), 53 deletions(-) diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 880845e..8d621bb 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -395,71 +395,94 @@ class ApiCall { * @throws RundeckApiLoginException if the login failed */ private String login(HttpClient httpClient) throws RundeckApiLoginException { - String location = client.getUrl() + "/j_security_check"; String sessionID = null; - while (true) { - HttpPost postLogin = new HttpPost(location); - List params = new ArrayList(); - params.add(new BasicNameValuePair("j_username", client.getLogin())); - params.add(new BasicNameValuePair("j_password", client.getPassword())); - params.add(new BasicNameValuePair("action", "login")); - HttpResponse response = null; - try { - postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); - response = httpClient.execute(postLogin); - Header cookieHeader = response.getFirstHeader("Set-Cookie"); - if(cookieHeader != null){ - String cookieStr = cookieHeader.getValue(); - if(cookieStr != null){ - int i1 = cookieStr.indexOf("JSESSIONID="); - if(i1 >= 0){ - cookieStr = cookieStr.substring(i1 + "JSESSIONID=".length()); - int i2 = cookieStr.indexOf(";"); - if(i2 >= 0){ - sessionID = cookieStr.substring(0, i2); - } + // 1. call expected GET request + String location = client.getUrl(); + + try { + HttpGet getRequest = new HttpGet(location); + HttpResponse response = httpClient.execute(getRequest); + + // sessionID stored in case user wants to cache it for reuse + Header cookieHeader = response.getFirstHeader("Set-Cookie"); + if (cookieHeader != null) { + String cookieStr = cookieHeader.getValue(); + if (cookieStr != null) { + int i1 = cookieStr.indexOf("JSESSIONID="); + if (i1 >= 0) { + cookieStr = cookieStr.substring(i1 + "JSESSIONID=".length()); + int i2 = cookieStr.indexOf(";"); + if (i2 >= 0) { + sessionID = cookieStr.substring(0, i2); } } } + } + + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to get request on " + location, e); + } + + // 2. then call POST login request + location += "/j_security_check"; + + while (true) { + try { + HttpPost postLogin = new HttpPost(location); + List params = new ArrayList(); + params.add(new BasicNameValuePair("j_username", client.getLogin())); + params.add(new BasicNameValuePair("j_password", client.getPassword())); + params.add(new BasicNameValuePair("action", "login")); + postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); + HttpResponse response = httpClient.execute(postLogin); + + if (response.getStatusLine().getStatusCode() / 100 == 3) { + // HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually... + location = response.getFirstHeader("Location").getValue(); + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + continue; + } + + if (response.getStatusLine().getStatusCode() / 100 != 2) { + throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for " + + location); + } + + try { + String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8); + if (StringUtils.contains(content, "j_security_check")) { + throw new RundeckApiLoginException("Login failed for user " + client.getLogin()); + } + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + break; + } catch (IOException io) { + throw new RundeckApiLoginException("Failed to read RunDeck result", io); + } catch (ParseException p) { + throw new RundeckApiLoginException("Failed to parse RunDeck response", p); + } } catch (IOException e) { throw new RundeckApiLoginException("Failed to post login form on " + location, e); } - - if (response.getStatusLine().getStatusCode() / 100 == 3) { - // HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually... - location = response.getFirstHeader("Location").getValue(); - try { - EntityUtils.consume(response.getEntity()); - } catch (IOException e) { - throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); - } - continue; - } - if (response.getStatusLine().getStatusCode() / 100 != 2) { - throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for " - + location); - } - try { - String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8); - if (StringUtils.contains(content, "j_security_check")) { - throw new RundeckApiLoginException("Login failed for user " + client.getLogin()); - } - try { - EntityUtils.consume(response.getEntity()); - } catch (IOException e) { - throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); - } - } catch (IOException io) { - throw new RundeckApiLoginException("Failed to read RunDeck result", io); - } catch (ParseException p) { - throw new RundeckApiLoginException("Failed to parse RunDeck response", p); - } - break; } + return sessionID; } + /** * Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates * From 4027040e955359782847a49b8d0d0964a6574de1 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Fri, 24 Jan 2014 16:04:01 -0800 Subject: [PATCH 07/89] Add change info for 9.2 and 9.3 release --- src/changes/changes.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 060d462..7a679ae 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,16 @@ Vincent Behar + + + Issue #7: Fix authentication to Tomcat container (thanks @katanafleet) + + + + + Bugfix. If apiVersion is set to 0, request defaults to current API version. + + Add uuidOption support to jobs import, API v9 From 962acecabb91ada00846dcd2cab1d8bacf8e9c44 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Fri, 24 Jan 2014 16:23:45 -0800 Subject: [PATCH 08/89] [maven-release-plugin] prepare release rundeck-api-java-client-9.3 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b0857ca..7611fe3 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.rundeck rundeck-api-java-client - 9.3-SNAPSHOT + 9.3 jar RunDeck API - Java Client Java client for the RunDeck REST API From 9f8bf879ffb4efea5f1cbc457a88092d1720d2f2 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Fri, 24 Jan 2014 16:23:50 -0800 Subject: [PATCH 09/89] [maven-release-plugin] prepare for next development iteration --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7611fe3..501ebe6 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.rundeck rundeck-api-java-client - 9.3 + 9.4-SNAPSHOT jar RunDeck API - Java Client Java client for the RunDeck REST API From eb392dd32eed5873470854b7ce8b315e9b2c8b87 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 13:17:30 -0800 Subject: [PATCH 10/89] Update status with api v10 todos --- src/site/confluence/status.confluence | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence index 4807a5c..8b64daf 100644 --- a/src/site/confluence/status.confluence +++ b/src/site/confluence/status.confluence @@ -83,3 +83,11 @@ h2. RunDeck API version 9 * list running executions across all projects - OK * include project name in execution results - OK * Add uuidOption parameter to allow removing imported UUIDs to avoid creation conflicts - OK +h2. RunDeck API version 10 + +[Documentation of the RunDeck API version 10|http://rundeck.org/2.0.0/api/index.html] + +* Execution State - Retrieve workflow step and node state information - *TODO* +* Execution Output with State - Retrieve log output with state change information - *TODO* +* Execution Output - Retrieve log output for a particular node or step - *TODO* +* Execution Info - added successfulNodes and failedNodes detail. - *TODO* From e3291e3dab8f02c691ae30129756738376d49a5e Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 15:12:38 -0800 Subject: [PATCH 11/89] Support successfulNodes/failedNodes in execution data for api v10 --- .../rundeck/api/domain/RundeckExecution.java | 24 +++++++++- .../org/rundeck/api/domain/RundeckNode.java | 3 +- .../api/domain/RundeckNodeIdentity.java | 14 ++++++ .../rundeck/api/parser/ExecutionParser.java | 26 ++++++++++ .../api/parser/ExecutionParserTest.java | 47 +++++++++++++++++++ .../api/parser/execution-result-v10.xml | 27 +++++++++++ 6 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/rundeck/api/domain/RundeckNodeIdentity.java create mode 100644 src/test/resources/org/rundeck/api/parser/execution-result-v10.xml diff --git a/src/main/java/org/rundeck/api/domain/RundeckExecution.java b/src/main/java/org/rundeck/api/domain/RundeckExecution.java index 0005554..20dc45c 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckExecution.java +++ b/src/main/java/org/rundeck/api/domain/RundeckExecution.java @@ -17,6 +17,7 @@ package org.rundeck.api.domain; import java.io.Serializable; import java.util.Date; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.lang.time.DurationFormatUtils; @@ -52,6 +53,8 @@ public class RundeckExecution implements Serializable { private String description; private String argstring; private String project; + private Set successfulNodes; + private Set failedNodes; /** * @return the duration of the execution in milliseconds (or null if the duration is still running, or has been @@ -174,7 +177,10 @@ public class RundeckExecution implements Serializable { return "RundeckExecution [id=" + id + ", description=" + description + ", url=" + url + ", status=" + status + ", argstring=" + argstring + ", startedBy=" + startedBy + ", startedAt=" + startedAt + ", endedAt=" + endedAt - + ", durationInSeconds=" + getDurationInSeconds() + ", abortedBy=" + abortedBy + ", job=" + job + "]"; + + ", durationInSeconds=" + getDurationInSeconds() + ", abortedBy=" + abortedBy + ", job=" + job + + ", successfulNodes=" + getSuccessfulNodes() + + ", failedNodes=" + getFailedNodes() + + "]"; } @Override @@ -281,6 +287,22 @@ public class RundeckExecution implements Serializable { this.project = project; } + public Set getSuccessfulNodes() { + return successfulNodes; + } + + public void setSuccessfulNodes(Set successfulNodes) { + this.successfulNodes = successfulNodes; + } + + public Set getFailedNodes() { + return failedNodes; + } + + public void setFailedNodes(Set failedNodes) { + this.failedNodes = failedNodes; + } + /** * The status of an execution */ diff --git a/src/main/java/org/rundeck/api/domain/RundeckNode.java b/src/main/java/org/rundeck/api/domain/RundeckNode.java index 87a1a36..9d54b9f 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckNode.java +++ b/src/main/java/org/rundeck/api/domain/RundeckNode.java @@ -23,7 +23,7 @@ import java.util.List; * * @author Vincent Behar */ -public class RundeckNode implements Serializable { +public class RundeckNode implements Serializable, RundeckNodeIdentity { private static final long serialVersionUID = 1L; @@ -63,6 +63,7 @@ public class RundeckNode implements Serializable { /** URL to an external resource model service. (optional) */ private String remoteUrl; + @Override public String getName() { return name; } diff --git a/src/main/java/org/rundeck/api/domain/RundeckNodeIdentity.java b/src/main/java/org/rundeck/api/domain/RundeckNodeIdentity.java new file mode 100644 index 0000000..063bfd3 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckNodeIdentity.java @@ -0,0 +1,14 @@ +package org.rundeck.api.domain; + + +/** + * Identifies a node by name + */ +public interface RundeckNodeIdentity { + /** + * Return the rundeck Node name + * + * @return + */ + String getName(); +} diff --git a/src/main/java/org/rundeck/api/parser/ExecutionParser.java b/src/main/java/org/rundeck/api/parser/ExecutionParser.java index 63ea0d1..90a6e8d 100644 --- a/src/main/java/org/rundeck/api/parser/ExecutionParser.java +++ b/src/main/java/org/rundeck/api/parser/ExecutionParser.java @@ -15,12 +15,18 @@ */ package org.rundeck.api.parser; +import java.util.Collections; import java.util.Date; +import java.util.HashSet; +import java.util.List; + import org.apache.commons.lang.StringUtils; import org.dom4j.Node; import org.rundeck.api.domain.RundeckExecution; import org.rundeck.api.domain.RundeckJob; import org.rundeck.api.domain.RundeckExecution.ExecutionStatus; +import org.rundeck.api.domain.RundeckNode; +import org.rundeck.api.domain.RundeckNodeIdentity; /** * Parser for a single {@link RundeckExecution} @@ -76,6 +82,26 @@ public class ExecutionParser implements XmlNodeParser { execution.setJob(job); } + final Node successfulNodes = execNode.selectSingleNode("successfulNodes"); + if (successfulNodes != null) { + final List rundeckNodes = + new ListParser(new NodeParser(), "successfulNodes/node") + .parseXmlNode(execNode); + execution.setSuccessfulNodes(new HashSet(rundeckNodes)); + }else{ + execution.setSuccessfulNodes(Collections.emptySet()); + } + + final Node failedNodes = execNode.selectSingleNode("failedNodes"); + if (failedNodes != null) { + final List rundeckNodes = + new ListParser(new NodeParser(), "failedNodes/node") + .parseXmlNode(execNode); + execution.setFailedNodes(new HashSet(rundeckNodes)); + } else { + execution.setFailedNodes(Collections.emptySet()); + } + return execution; } diff --git a/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java b/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java index 0e4daa4..b34cbb5 100644 --- a/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java +++ b/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java @@ -16,13 +16,17 @@ package org.rundeck.api.parser; import java.io.InputStream; +import java.util.Arrays; import java.util.Date; +import java.util.HashSet; + import org.dom4j.Document; import org.junit.Assert; import org.junit.Test; import org.rundeck.api.domain.RundeckExecution; import org.rundeck.api.domain.RundeckJob; import org.rundeck.api.domain.RundeckExecution.ExecutionStatus; +import org.rundeck.api.domain.RundeckNodeIdentity; /** * Test the {@link ExecutionParser} @@ -156,4 +160,47 @@ public class ExecutionParserTest { } + @Test + public void parseV10Execution() throws Exception { + InputStream input = getClass().getResourceAsStream("execution-result-v10.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckExecution execution = new ExecutionParser("result/executions/execution").parseXmlNode(document); + RundeckJob job = execution.getJob(); + + Assert.assertNotNull(job); + Assert.assertEquals(new Long(146), execution.getId()); + Assert.assertEquals("http://dignan.local:4440/execution/follow/146", execution.getUrl()); + Assert.assertEquals(ExecutionStatus.SUCCEEDED, execution.getStatus()); + Assert.assertEquals("admin", execution.getStartedBy()); + Assert.assertEquals(new Date(1389894502959L), execution.getStartedAt()); + Assert.assertEquals(new Date(1389894504561L), execution.getEndedAt()); + Assert.assertEquals((Long)(1389894504561L- 1389894502959L), execution.getDurationInMillis()); + Assert.assertEquals(null, execution.getAbortedBy()); + Assert.assertEquals("fdfd", execution.getProject()); + + Assert.assertNotNull(execution.getSuccessfulNodes()); + Assert.assertEquals(3, execution.getSuccessfulNodes().size()); + + HashSet expectedSuccess = new HashSet(); + expectedSuccess.addAll(Arrays.asList( + "node-111.qa.subgroup.mycompany.com", + "node-6.qa.subgroup.mycompany.com", + "node-14.qa.subgroup.mycompany.com")); + for (RundeckNodeIdentity rundeckNodeIdentity : execution.getSuccessfulNodes()) { + Assert.assertTrue(expectedSuccess.contains(rundeckNodeIdentity.getName())); + } + + Assert.assertNotNull(execution.getFailedNodes()); + Assert.assertEquals(3, execution.getFailedNodes().size()); + HashSet expectedFailure = new HashSet(); + expectedFailure.addAll(Arrays.asList( + "node-112.qa.subgroup.mycompany.com", + "node-62.qa.subgroup.mycompany.com", + "node-12.qa.subgroup.mycompany.com")); + for (RundeckNodeIdentity rundeckNodeIdentity : execution.getFailedNodes()) { + Assert.assertTrue(expectedFailure.contains(rundeckNodeIdentity.getName())); + } + } + } diff --git a/src/test/resources/org/rundeck/api/parser/execution-result-v10.xml b/src/test/resources/org/rundeck/api/parser/execution-result-v10.xml new file mode 100644 index 0000000..9c3db6d --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/execution-result-v10.xml @@ -0,0 +1,27 @@ + + + + admin + 2014-01-16T17:48:22Z + 2014-01-16T17:48:24Z + + test 1 + + fdfd + + + echo hi there + + + + + + + + + + + + + + From 9ca85e810ff00b4d0c60e3bad00405b908a68c65 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 16:01:21 -0800 Subject: [PATCH 12/89] Use api version 10 --- src/main/java/org/rundeck/api/RundeckClient.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index b218702..8c2c0e0 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -101,6 +101,7 @@ public class RundeckClient implements Serializable { V7(7), V8(8), V9(9), + V10(10), ; private int versionNumber; @@ -114,7 +115,7 @@ public class RundeckClient implements Serializable { } } /** Version of the API supported */ - public static final transient int API_VERSION = Version.V9.getVersionNumber(); + public static final transient int API_VERSION = Version.V10.getVersionNumber(); private static final String API = "/api/"; From af37fd1f56e451e172ea8222b5b8bd5756b62e89 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 16:04:48 -0800 Subject: [PATCH 13/89] Add default Accept header text/xml to requests, allow overriding it --- src/main/java/org/rundeck/api/ApiCall.java | 22 +++++++++++++++---- .../java/org/rundeck/api/ApiPathBuilder.java | 16 ++++++++++++++ .../betamax/tapes/get_executions.yaml | 20 +++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 8d621bb..eec4701 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -177,7 +177,11 @@ class ApiCall { */ public T get(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - return execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath), parser); + HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath); + if (null != apiPath.getAccept()) { + request.setHeader("Accept", apiPath.getAccept()); + } + return execute(request, parser); } /** @@ -192,7 +196,11 @@ class ApiCall { */ public InputStream get(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath)); + HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath); + if (null != apiPath.getAccept()) { + request.setHeader("Accept", apiPath.getAccept()); + } + ByteArrayInputStream response = execute(request); // try to load the document, to throw an exception in case of error ParserHelper.loadDocument(response); @@ -213,7 +221,11 @@ class ApiCall { */ public InputStream getNonApi(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { - ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + apiPath)); + HttpGet request = new HttpGet(client.getUrl() + apiPath); + if (null != apiPath.getAccept()) { + request.setHeader("Accept", apiPath.getAccept()); + } + ByteArrayInputStream response = execute(request); response.reset(); return response; @@ -257,7 +269,9 @@ class ApiCall { public T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException { HttpPost httpPost = new HttpPost(client.getUrl() + client.getApiEndpoint() + apiPath); - + if(null!= apiPath.getAccept()) { + httpPost.setHeader("Accept", apiPath.getAccept()); + } // POST a multi-part request, with all attachments if(apiPath.getAttachments().size()>0){ MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java index 0fe823c..22643c8 100644 --- a/src/main/java/org/rundeck/api/ApiPathBuilder.java +++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java @@ -38,6 +38,8 @@ class ApiPathBuilder { /** Internally, we store everything in a {@link StringBuilder} */ private final StringBuilder apiPath; + private String accept="text/xml"; + /** When POSTing, we can add attachments */ private final Map attachments; private final List form = new ArrayList(); @@ -63,6 +65,13 @@ class ApiPathBuilder { } } + /** + * Set the accept header + */ + public ApiPathBuilder accept(String mimeTypes) { + accept=mimeTypes; + return this; + } /** * Visit a {@link BuildsParameters} and add the parameters */ @@ -305,6 +314,13 @@ class ApiPathBuilder { return getAttachments().size() > 0 || getForm().size() > 0; } + /** + * Accept header value, default "text/xml" + */ + public String getAccept() { + return accept; + } + /** * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder} * diff --git a/src/test/resources/betamax/tapes/get_executions.yaml b/src/test/resources/betamax/tapes/get_executions.yaml index 337968b..740a0c5 100644 --- a/src/test/resources/betamax/tapes/get_executions.yaml +++ b/src/test/resources/betamax/tapes/get_executions.yaml @@ -6,6 +6,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?jobFilter=test+job&project=blah&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -23,6 +24,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&descFilter=a+description&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -38,6 +40,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&begin=2012-09-13T17%3A06%3A18Z&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -53,6 +56,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&end=2012-09-13T17%3A06%3A18Z&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -68,6 +72,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&excludeJobIdListFilter=123&excludeJobIdListFilter=456&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -83,6 +88,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&jobListFilter=fruit%2Fmango&jobListFilter=fruit%2Flemon&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -98,6 +104,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&excludeJobListFilter=a%2Fpath%2Fjob1&excludeJobListFilter=path%2Fto%2Fjob2&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -113,6 +120,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&jobIdListFilter=1f4415d7-3b52-4fc8-ba42-b6ac97508bff&jobIdListFilter=d9fc5ee6-f1db-4d24-8808-feda18345bab&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -128,6 +136,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&groupPath=fruit&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -143,6 +152,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&groupPathExact=fruit&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -158,6 +168,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?jobExactFilter=test+job&project=blah&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -173,6 +184,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&loglevelFilter=INFO&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -188,6 +200,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&recentFilter=1h&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -203,6 +216,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&statusFilter=succeeded&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -218,6 +232,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&adhoc=true&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -235,6 +250,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&abortedbyFilter=admin&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -252,6 +268,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?excludeJobFilter=test+job&project=blah&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -269,6 +286,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?excludeJobExactFilter=test+job&project=blah&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -284,6 +302,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&excludeGroupPath=fruit&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 @@ -301,6 +320,7 @@ interactions: method: GET uri: http://rundeck.local:4440/api/5/executions?project=blah&excludeGroupPathExact=fruit&max=2&offset=0 headers: + Accept: text/xml Host: rundeck.local:4440 Proxy-Connection: Keep-Alive User-Agent: RunDeck API Java Client 5 From aba9f8de2b0f16fb670cd7507a19b2ab22883ee3 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 16:05:52 -0800 Subject: [PATCH 14/89] Use accept header in in /jobs/export request --- src/main/java/org/rundeck/api/RundeckClient.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 8c2c0e0..28bf88b 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -542,11 +542,13 @@ public class RundeckClient implements Serializable { throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(format, "format is mandatory to export jobs !"); AssertUtil.notBlank(project, "project is mandatory to export jobs !"); - return new ApiCall(this).get(new ApiPathBuilder("/jobs/export").param("format", format) - .param("project", project) - .param("jobFilter", jobFilter) - .param("groupPath", groupPath) - .param("idlist", StringUtils.join(jobIds, ","))); + return new ApiCall(this).get(new ApiPathBuilder("/jobs/export") + .accept(format == FileType.XML ? "text/xml" : "text/yaml") + .param("format", format) + .param("project", project) + .param("jobFilter", jobFilter) + .param("groupPath", groupPath) + .param("idlist", StringUtils.join(jobIds, ","))); } /** From 8e59972a799ff5faa52ffe7123bfed5ae77ffafc Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 16:06:21 -0800 Subject: [PATCH 15/89] Add node and step criteria for execution output for api v10 --- .../java/org/rundeck/api/RundeckClient.java | 120 +++++++++++++++++- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 28bf88b..f705c48 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -3265,11 +3265,119 @@ public class RundeckClient implements Serializable { public RundeckOutput getJobExecutionOutput(Long executionId, int offset, int lastlines, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/output.xml").param("offset", offset) - .param("lastlines", lastlines) - .param("lastmod", lastmod) - .param("maxlines", maxlines), - new OutputParser("result/output", createOutputEntryParser())); + return new ApiCall(this).get(new ApiPathBuilder( + "/execution/", executionId.toString(), + "/output") + .param("offset", offset) + .param("lastlines", lastlines) + .param("lastmod", lastmod) + .param("maxlines", maxlines), + new OutputParser("result/output", createOutputEntryParser())); + } + + /** + * Get the execution output of the given execution on the specified node + * + * @param executionId identifier of the execution - mandatory + * @param nodeName name of the node + * @param offset byte offset to read from in the file. 0 indicates the beginning. + * @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. + * @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 + * @param maxlines maximum number of lines to retrieve forward from the specified offset. + * + * @return {@link RundeckOutput} + * + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckOutput getJobExecutionOutputForNode(Long executionId, String nodeName, int offset, int lastlines, + long lastmod, int maxlines) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); + AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!"); + return new ApiCall(this).get(new ApiPathBuilder( + "/execution/", executionId.toString(), + "/output/node/", nodeName ) + .param("offset", offset) + .param("lastlines", lastlines) + .param("lastmod", lastmod) + .param("maxlines", maxlines), + new OutputParser("result/output", createOutputEntryParser())); + } + /** + * Get the execution output of the given execution for the specified step + * + * @param executionId identifier of the execution - mandatory + * @param stepCtx identifier for the step + * @param offset byte offset to read from in the file. 0 indicates the beginning. + * @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. + * @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 + * @param maxlines maximum number of lines to retrieve forward from the specified offset. + * + * @return {@link RundeckOutput} + * + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckOutput getJobExecutionOutputForStep(Long executionId, String stepCtx, int offset, int lastlines, + long lastmod, int maxlines) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); + AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!"); + return new ApiCall(this).get(new ApiPathBuilder( + "/execution/", executionId.toString(), + "/output/step/", stepCtx) + .param("offset", offset) + .param("lastlines", lastlines) + .param("lastmod", lastmod) + .param("maxlines", maxlines), + new OutputParser("result/output", createOutputEntryParser())); + } + /** + * Get the execution output of the given execution for the specified step + * + * @param executionId identifier of the execution - mandatory + * @param stepCtx identifier for the step + * @param offset byte offset to read from in the file. 0 indicates the beginning. + * @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. + * @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 + * @param maxlines maximum number of lines to retrieve forward from the specified offset. + * + * @return {@link RundeckOutput} + * + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckOutput getJobExecutionOutputForNodeAndStep(Long executionId, String nodeName, String stepCtx, int offset, int lastlines, + long lastmod, int maxlines) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); + AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!"); + AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!"); + return new ApiCall(this).get(new ApiPathBuilder( + "/execution/", executionId.toString(), + "/output/node/", nodeName, + "/step/", stepCtx) + .param("offset", offset) + .param("lastlines", lastlines) + .param("lastmod", lastmod) + .param("maxlines", maxlines), + new OutputParser("result/output", createOutputEntryParser())); } @@ -3289,7 +3397,7 @@ public class RundeckClient implements Serializable { public RundeckOutput getJobExecutionOutput(Long executionId, int offset, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/output.xml").param("offset", offset) + return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/output").param("offset", offset) .param("lastmod", lastmod) .param("maxlines", maxlines), new OutputParser("result/output", createOutputEntryParser())); From cb4d67f8f4edb8fb7ba9f663f6f12adc5530e8c1 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 17:24:14 -0800 Subject: [PATCH 16/89] Execution output supports node and step context selection for api v10 --- .../java/org/rundeck/api/RundeckClient.java | 83 +++++++++----- .../org/rundeck/api/domain/RundeckOutput.java | 47 +++++++- .../api/domain/RundeckOutputEntry.java | 58 ++++++++-- .../rundeck/api/parser/OutputEntryParser.java | 53 ++++++++- .../org/rundeck/api/parser/OutputParser.java | 7 +- .../org/rundeck/api/RundeckClientTest.java | 101 ++++++++++++++++++ .../api/parser/OutputEntryParserTest.java | 55 ++++++++++ .../rundeck/api/parser/OutputParserTest.java | 85 +++++++++++++++ .../betamax/tapes/execution_output_basic.yaml | 24 +++++ .../tapes/execution_output_fornode.yaml | 23 ++++ .../execution_output_fornodeandstep.yaml | 23 ++++ .../tapes/execution_output_forstep.yaml | 24 +++++ .../rundeck/api/parser/output-filtered.xml | 19 ++++ .../org/rundeck/api/parser/output-state.xml | 46 ++++++++ .../rundeck/api/parser/output-unmodified.xml | 16 +++ .../org/rundeck/api/parser/output1.xml | 22 ++++ 16 files changed, 647 insertions(+), 39 deletions(-) create mode 100644 src/test/java/org/rundeck/api/parser/OutputEntryParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/OutputParserTest.java create mode 100644 src/test/resources/betamax/tapes/execution_output_basic.yaml create mode 100644 src/test/resources/betamax/tapes/execution_output_fornode.yaml create mode 100644 src/test/resources/betamax/tapes/execution_output_fornodeandstep.yaml create mode 100644 src/test/resources/betamax/tapes/execution_output_forstep.yaml create mode 100644 src/test/resources/org/rundeck/api/parser/output-filtered.xml create mode 100644 src/test/resources/org/rundeck/api/parser/output-state.xml create mode 100644 src/test/resources/org/rundeck/api/parser/output-unmodified.xml create mode 100644 src/test/resources/org/rundeck/api/parser/output1.xml diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index f705c48..6c0d248 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -3265,13 +3265,20 @@ public class RundeckClient implements Serializable { public RundeckOutput getJobExecutionOutput(Long executionId, int offset, int lastlines, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder( + ApiPathBuilder param = new ApiPathBuilder( "/execution/", executionId.toString(), "/output") - .param("offset", offset) - .param("lastlines", lastlines) - .param("lastmod", lastmod) - .param("maxlines", maxlines), + .param("offset", offset); + if (lastlines > 0) { + param.param("lastlines", lastlines); + } + if (lastmod >= 0) { + param.param("lastmod", lastmod); + } + if (maxlines > 0) { + param.param("maxlines", maxlines); + } + return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } @@ -3300,13 +3307,20 @@ public class RundeckClient implements Serializable { throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder( + ApiPathBuilder param = new ApiPathBuilder( "/execution/", executionId.toString(), - "/output/node/", nodeName ) - .param("offset", offset) - .param("lastlines", lastlines) - .param("lastmod", lastmod) - .param("maxlines", maxlines), + "/output/node/", nodeName) + .param("offset", offset); + if(lastlines>0) { + param.param("lastlines", lastlines); + } + if(lastmod>=0) { + param.param("lastmod", lastmod); + } + if(maxlines>0) { + param.param("maxlines", maxlines); + } + return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } /** @@ -3334,13 +3348,20 @@ public class RundeckClient implements Serializable { throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder( + ApiPathBuilder param = new ApiPathBuilder( "/execution/", executionId.toString(), "/output/step/", stepCtx) - .param("offset", offset) - .param("lastlines", lastlines) - .param("lastmod", lastmod) - .param("maxlines", maxlines), + .param("offset", offset); + if (lastlines > 0) { + param.param("lastlines", lastlines); + } + if (lastmod >= 0) { + param.param("lastmod", lastmod); + } + if (maxlines > 0) { + param.param("maxlines", maxlines); + } + return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } /** @@ -3369,14 +3390,21 @@ public class RundeckClient implements Serializable { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!"); AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder( + ApiPathBuilder param = new ApiPathBuilder( "/execution/", executionId.toString(), "/output/node/", nodeName, "/step/", stepCtx) - .param("offset", offset) - .param("lastlines", lastlines) - .param("lastmod", lastmod) - .param("maxlines", maxlines), + .param("offset", offset); + if (lastlines > 0) { + param.param("lastlines", lastlines); + } + if (lastmod >= 0) { + param.param("lastmod", lastmod); + } + if (maxlines > 0) { + param.param("maxlines", maxlines); + } + return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } @@ -3397,10 +3425,15 @@ public class RundeckClient implements Serializable { public RundeckOutput getJobExecutionOutput(Long executionId, int offset, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); - return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/output").param("offset", offset) - .param("lastmod", lastmod) - .param("maxlines", maxlines), - new OutputParser("result/output", createOutputEntryParser())); + ApiPathBuilder param = new ApiPathBuilder("/execution/", executionId.toString(), "/output") + .param("offset", offset); + if (lastmod >= 0) { + param.param("lastmod", lastmod); + } + if (maxlines > 0) { + param.param("maxlines", maxlines); + } + return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } private OutputEntryParser createOutputEntryParser() { diff --git a/src/main/java/org/rundeck/api/domain/RundeckOutput.java b/src/main/java/org/rundeck/api/domain/RundeckOutput.java index a4696ec..3663810 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckOutput.java +++ b/src/main/java/org/rundeck/api/domain/RundeckOutput.java @@ -22,9 +22,9 @@ public class RundeckOutput implements Serializable { //private String error = null; - private Boolean unmodified; + private Boolean unmodified = false; - private Boolean empty; + private Boolean empty = false; private int offset; @@ -46,6 +46,9 @@ public class RundeckOutput implements Serializable { List logEntries = null; + private String filterNode; + private String filterStep; + public Long getExecutionId() { return executionId; } @@ -181,6 +184,7 @@ public class RundeckOutput implements Serializable { ", execCompleted=" + execCompleted + ", hasFailedNodes=" + hasFailedNodes + ", status=" + status + ", lastModified=" + lastModified + ", execDuration=" + execDuration + ", percentLoaded=" + percentLoaded + + ", filterNode=" + filterNode + ", filterStep=" + filterStep + ", totalSize=" + totalSize + "]"; } @@ -203,6 +207,8 @@ public class RundeckOutput implements Serializable { result = prime * result + ((percentLoaded == null) ? 0 : percentLoaded.hashCode()); result = prime * result + totalSize; result = prime * result + ((logEntries == null) ? 0 : logEntries.hashCode()); + result = prime * result + ((filterNode == null) ? 0 : filterNode.hashCode()); + result = prime * result + ((filterStep == null) ? 0 : filterStep.hashCode()); return result; } @@ -286,7 +292,40 @@ public class RundeckOutput implements Serializable { return false; } else if (!logEntries.equals(other.logEntries)) return false; + if (filterNode == null) { + if (other.filterNode != null) + return false; + } else if (!filterNode.equals(other.filterNode)) + return false; + if (filterStep == null) { + if (other.filterStep != null) + return false; + } else if (!filterStep.equals(other.filterStep)) + return false; return true; } - -} \ No newline at end of file + + public String getFilterNode() { + return filterNode; + } + + /** + * return the node name used to filter this output + * @param filterNode + */ + public void setFilterNode(String filterNode) { + this.filterNode = filterNode; + } + + /** + * Return the step context used to filter this output + * @return + */ + public String getFilterStep() { + return filterStep; + } + + public void setFilterStep(String filterStep) { + this.filterStep = filterStep; + } +} diff --git a/src/main/java/org/rundeck/api/domain/RundeckOutputEntry.java b/src/main/java/org/rundeck/api/domain/RundeckOutputEntry.java index 607c01a..4160634 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckOutputEntry.java +++ b/src/main/java/org/rundeck/api/domain/RundeckOutputEntry.java @@ -1,6 +1,8 @@ package org.rundeck.api.domain; import java.io.Serializable; +import java.util.Date; +import java.util.Map; /** * Represents a RunDeck output entry @@ -11,7 +13,8 @@ public class RundeckOutputEntry implements Serializable { private static final long serialVersionUID = 1L; private String time = null; - + private Date absoluteTime = null; + private RundeckLogLevel level = null; private String message = null; @@ -21,7 +24,9 @@ public class RundeckOutputEntry implements Serializable { private String command = null; private String node = null; + private String type = null; + private Map metadata; public String getTime() { @@ -77,8 +82,11 @@ public class RundeckOutputEntry implements Serializable { @Override public String toString() { return "RundeckOutputEntry [time=" + time + ", level=" + level + - ", message=" + message + ", user=" + user + ", command=" + - command + ", node=" + node + "]"; + ", message=" + message + ", user=" + user + + ", command=" + command + + ", type=" + type + + ", metadata=" + metadata + + ", node=" + node + "]"; } @Override @@ -91,6 +99,8 @@ public class RundeckOutputEntry implements Serializable { result = prime * result + ((user == null) ? 0 : user.hashCode()); result = prime * result + ((command == null) ? 0 : command.hashCode()); result = prime * result + ((node == null) ? 0 : node.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + result = prime * result + ((metadata == null) ? 0 : metadata.hashCode()); return result; } @@ -134,13 +144,49 @@ public class RundeckOutputEntry implements Serializable { return false; } else if (!node.equals(other.node)) return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + if (metadata == null) { + if (other.metadata != null) + return false; + } else if (!metadata.equals(other.metadata)) + return false; return true; } + /** + * type of entry + */ + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public Date getAbsoluteTime() { + return absoluteTime; + } + + public void setAbsoluteTime(Date absoluteTime) { + this.absoluteTime = absoluteTime; + } + public static enum RundeckLogLevel { - SEVERE, WARNING, INFO, CONFIG, FINEST; + SEVERE, ERROR, WARNING, INFO, NORMAL, DEBUG, CONFIG, VERBOSE, FINEST,; } - -} \ No newline at end of file +} diff --git a/src/main/java/org/rundeck/api/parser/OutputEntryParser.java b/src/main/java/org/rundeck/api/parser/OutputEntryParser.java index 9754377..36fbca8 100644 --- a/src/main/java/org/rundeck/api/parser/OutputEntryParser.java +++ b/src/main/java/org/rundeck/api/parser/OutputEntryParser.java @@ -6,6 +6,10 @@ import org.dom4j.Node; import org.rundeck.api.domain.RundeckOutputEntry; import org.rundeck.api.domain.RundeckOutputEntry.RundeckLogLevel; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + /** * Parses output message content for API v6 */ @@ -16,15 +20,28 @@ public class OutputEntryParser implements XmlNodeParser { public OutputEntryParser() { super(); + dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); } + private SimpleDateFormat dateFormat; /** * @param xpath of the event element if it is not the root node */ public OutputEntryParser(String xpath) { - super(); + this(); this.xpath = xpath; } + static HashSet nonMetaAttributes = new HashSet(); + static { + nonMetaAttributes.add("time"); + nonMetaAttributes.add("level"); + nonMetaAttributes.add("user"); + nonMetaAttributes.add("node"); + nonMetaAttributes.add("type"); + nonMetaAttributes.add("log"); + nonMetaAttributes.add("absolute_type"); + } @Override public RundeckOutputEntry parseXmlNode(Node node) { @@ -38,15 +55,45 @@ public class OutputEntryParser implements XmlNodeParser { } catch (IllegalArgumentException e) { outputEntry.setLevel(null); } + if(null!=entryNode.valueOf("@absolute_time")) { + outputEntry.setAbsoluteTime(parseDate(StringUtils.trimToNull(entryNode.valueOf("@absolute_time")))); + } outputEntry.setUser(StringUtils.trimToNull(entryNode.valueOf("@user"))); - outputEntry.setCommand(StringUtils.trimToNull(entryNode.valueOf("@command"))); outputEntry.setNode(StringUtils.trimToNull(entryNode.valueOf("@node"))); + outputEntry.setType(StringUtils.trimToNull(entryNode.valueOf("@type"))); + + HashMap meta = new HashMap(); + List list = entryNode.selectNodes("@*"); + for (Object node1 : list) { + if(node1 instanceof Node) { + Node child = (Node) node1; + if (!nonMetaAttributes.contains(child.getName())) { + meta.put(child.getName(), child.getText()); + } + } + } + if(meta.size()>0){ + outputEntry.setMetadata(meta); + } outputEntry.setMessage(parseMessage(entryNode)); return outputEntry; } + private Date parseDate(String s) { + if(null==s){ + return null; + } + try { + Date parse = dateFormat.parse(s); + System.out.println(s + ": " + parse.getTime()); + return parse; + } catch (ParseException e) { + return null; + } + } + /** * Parse the message content */ @@ -54,4 +101,4 @@ public class OutputEntryParser implements XmlNodeParser { return StringUtils.trimToNull(entryNode.valueOf("@log")); } -} \ No newline at end of file +} diff --git a/src/main/java/org/rundeck/api/parser/OutputParser.java b/src/main/java/org/rundeck/api/parser/OutputParser.java index f5fe2a2..bc988e2 100644 --- a/src/main/java/org/rundeck/api/parser/OutputParser.java +++ b/src/main/java/org/rundeck/api/parser/OutputParser.java @@ -47,6 +47,7 @@ public class OutputParser implements XmlNodeParser { output.setCompleted(Boolean.valueOf(entryNode.valueOf("completed"))); output.setExecCompleted(Boolean.valueOf(entryNode.valueOf("execCompleted"))); output.setHasFailedNodes(Boolean.valueOf(entryNode.valueOf("hasFailedNodes"))); + output.setUnmodified(Boolean.valueOf(entryNode.valueOf("unmodified"))); try { output.setStatus(RundeckExecution.ExecutionStatus @@ -78,6 +79,10 @@ public class OutputParser implements XmlNodeParser { } catch (NumberFormatException e) { output.setTotalSize(-1); } + if(entryNode.selectSingleNode("filter")!=null){ + output.setFilterNode(StringUtils.trimToNull(entryNode.valueOf("filter/@nodename"))); + output.setFilterStep(StringUtils.trimToNull(entryNode.valueOf("filter/@stepctx"))); + } Node entriesListNode = entryNode.selectSingleNode("entries"); @@ -93,4 +98,4 @@ public class OutputParser implements XmlNodeParser { return output; } -} \ No newline at end of file +} diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index c5330be..76cac2c 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -1034,6 +1034,107 @@ public class RundeckClientTest { RundeckExecution exec2 = runningExecutions.get(1); Assert.assertEquals("test2", exec2.getProject()); } + /** + * Execution output + */ + @Test + @Betamax(tape = "execution_output_basic", mode = TapeMode.READ_ONLY) + public void executionOutputBasic() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckOutput output = client.getJobExecutionOutput(146L,0,0L,-1); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals(null, output.getFilterNode()); + Assert.assertEquals(null, output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.57597), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(3, output.getLogEntries().size()); + } + /** + * Execution output for a node + */ + @Test + @Betamax(tape = "execution_output_fornode", mode = TapeMode.READ_ONLY) + public void executionOutputForNode() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckOutput output = client.getJobExecutionOutputForNode(146L,"node-14.qa.subgroup.mycompany.com",0,-1,0L,-1); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals("node-14.qa.subgroup.mycompany.com", output.getFilterNode()); + Assert.assertEquals(null, output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.57597), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(1, output.getLogEntries().size()); + } + /** + * Execution output for a step + */ + @Test + @Betamax(tape = "execution_output_forstep", mode = TapeMode.READ_ONLY) + public void executionOutputForStep() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckOutput output = client.getJobExecutionOutputForStep(146L,"1",0,-1,0L,-1); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals(null, output.getFilterNode()); + Assert.assertEquals("1", output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.57597), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(3, output.getLogEntries().size()); + } + + /** + * Execution output for a node and step + */ + @Test + @Betamax(tape = "execution_output_fornodeandstep", mode = TapeMode.READ_ONLY) + public void executionOutputForNodeAndStep() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckOutput output = client.getJobExecutionOutputForNodeAndStep(146L,"node-14.qa.subgroup.mycompany.com","1",0,-1,0L,-1); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals("node-14.qa.subgroup.mycompany.com", output.getFilterNode()); + Assert.assertEquals("1", output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.57597), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(1, output.getLogEntries().size()); + } @Before public void setUp() throws Exception { diff --git a/src/test/java/org/rundeck/api/parser/OutputEntryParserTest.java b/src/test/java/org/rundeck/api/parser/OutputEntryParserTest.java new file mode 100644 index 0000000..40b1155 --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/OutputEntryParserTest.java @@ -0,0 +1,55 @@ +package org.rundeck.api.parser; + +import org.dom4j.Document; +import org.junit.Assert; +import org.junit.Test; +import org.rundeck.api.domain.RundeckOutputEntry; + +import java.io.InputStream; +import java.util.Date; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 4:35 PM + */ +public class OutputEntryParserTest { + @Test + public void testEntryBasic() { + InputStream input = getClass().getResourceAsStream("output1.xml"); + Document document = ParserHelper.loadDocument(input); + OutputEntryParser outputEntryParser = new OutputEntryParser("//entry[1]"); + + RundeckOutputEntry rundeckOutputEntry = outputEntryParser.parseXmlNode(document); + Assert.assertEquals("hi there", rundeckOutputEntry.getMessage()); + Assert.assertEquals(null, rundeckOutputEntry.getCommand()); + Assert.assertEquals(RundeckOutputEntry.RundeckLogLevel.NORMAL, rundeckOutputEntry.getLevel()); + Assert.assertEquals("node-111.qa.subgroup.mycompany.com", rundeckOutputEntry.getNode()); + Assert.assertEquals("09:48:23", rundeckOutputEntry.getTime()); + Assert.assertEquals(new Date(1389894503000L), rundeckOutputEntry.getAbsoluteTime()); + Assert.assertEquals(null, rundeckOutputEntry.getType()); + Assert.assertEquals("Raif", rundeckOutputEntry.getUser()); + Assert.assertNotNull(rundeckOutputEntry.getMetadata()); + Assert.assertNotNull(rundeckOutputEntry.getMetadata().get("stepctx")); + Assert.assertEquals("1",rundeckOutputEntry.getMetadata().get("stepctx")); + } + @Test + public void testEntryState() { + InputStream input = getClass().getResourceAsStream("output-state.xml"); + Document document = ParserHelper.loadDocument(input); + OutputEntryParser outputEntryParser = new OutputEntryParser("//entry[1]"); + + RundeckOutputEntry rundeckOutputEntry = outputEntryParser.parseXmlNode(document); + Assert.assertEquals(null, rundeckOutputEntry.getMessage()); + Assert.assertEquals(null, rundeckOutputEntry.getCommand()); + Assert.assertEquals(RundeckOutputEntry.RundeckLogLevel.NORMAL, rundeckOutputEntry.getLevel()); + Assert.assertEquals("dignan", rundeckOutputEntry.getNode()); + Assert.assertEquals("09:48:23", rundeckOutputEntry.getTime()); + Assert.assertEquals(new Date(1389894503000L), rundeckOutputEntry.getAbsoluteTime()); + Assert.assertEquals("stepbegin", rundeckOutputEntry.getType()); + Assert.assertEquals("admin", rundeckOutputEntry.getUser()); + Assert.assertNotNull(rundeckOutputEntry.getMetadata()); + Assert.assertNotNull(rundeckOutputEntry.getMetadata().get("stepctx")); + Assert.assertEquals("1",rundeckOutputEntry.getMetadata().get("stepctx")); + Assert.assertNotNull(rundeckOutputEntry.getMetadata().get("step")); + Assert.assertEquals("1",rundeckOutputEntry.getMetadata().get("step")); + } +} diff --git a/src/test/java/org/rundeck/api/parser/OutputParserTest.java b/src/test/java/org/rundeck/api/parser/OutputParserTest.java new file mode 100644 index 0000000..dec471a --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/OutputParserTest.java @@ -0,0 +1,85 @@ +package org.rundeck.api.parser; + +import org.dom4j.Document; +import org.junit.Assert; +import org.junit.Test; +import org.rundeck.api.domain.RundeckExecution; +import org.rundeck.api.domain.RundeckOutput; +import org.rundeck.api.domain.RundeckProject; + +import java.io.InputStream; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 4:24 PM + */ +public class OutputParserTest { + @Test + public void parseOutputBasic() throws Exception { + InputStream input = getClass().getResourceAsStream("output1.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckOutput output = new OutputParser("result/output", new OutputEntryParser()).parseXmlNode(document); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals(null, output.getFilterNode()); + Assert.assertEquals(null, output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.9), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(3, output.getLogEntries().size()); + } + @Test + public void parseOutputFiltered() throws Exception { + InputStream input = getClass().getResourceAsStream("output-filtered.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckOutput output = new OutputParser("result/output", new OutputEntryParser()).parseXmlNode(document); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals("node-111.qa.subgroup.mycompany.com", output.getFilterNode()); + Assert.assertEquals("1", output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.9), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(1, output.getLogEntries().size()); + } + @Test + public void parseOutputUnmodified() throws Exception { + InputStream input = getClass().getResourceAsStream("output-unmodified.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckOutput output = new OutputParser("result/output", new OutputEntryParser()).parseXmlNode(document); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals(null, output.getFilterNode()); + Assert.assertEquals(null, output.getFilterStep()); + Assert.assertEquals(0, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(null, output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(true, output.isUnmodified()); + Assert.assertEquals(null, output.getLogEntries()); + } +} diff --git a/src/test/resources/betamax/tapes/execution_output_basic.yaml b/src/test/resources/betamax/tapes/execution_output_basic.yaml new file mode 100644 index 0000000..9315e74 --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_output_basic.yaml @@ -0,0 +1,24 @@ +!tape +name: execution_output_basic +interactions: +- recorded: 2014-01-17T01:12:05.218Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/146/output?offset=0&lastmod=0 + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1lnnniwx8tih2ehakkgipuyra;Path=/ + body: "\n \n 146\n 1409\n true\n true\n false\n succeeded\n\ + \ 1389894504000\n 1602\n 99.57597173144876\n 1415\n \n \n \n \n \n \n" diff --git a/src/test/resources/betamax/tapes/execution_output_fornode.yaml b/src/test/resources/betamax/tapes/execution_output_fornode.yaml new file mode 100644 index 0000000..3e62254 --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_output_fornode.yaml @@ -0,0 +1,23 @@ +!tape +name: execution_output_fornode +interactions: +- recorded: 2014-01-17T01:07:39.379Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/146/output/node/node-14.qa.subgroup.mycompany.com?offset=0&lastmod=0 + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=17y6a48eoxo7w4eqjrnba9xzz;Path=/ + body: "\n \n 146\n 1409\n true\n true\n false\n succeeded\n\ + \ 1389894504000\n 1602\n 99.57597173144876\n 1415\n \n \n\ + \ \n \n \n" diff --git a/src/test/resources/betamax/tapes/execution_output_fornodeandstep.yaml b/src/test/resources/betamax/tapes/execution_output_fornodeandstep.yaml new file mode 100644 index 0000000..1c5d5fa --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_output_fornodeandstep.yaml @@ -0,0 +1,23 @@ +!tape +name: execution_output_fornodeandstep +interactions: +- recorded: 2014-01-17T01:21:20.524Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/146/output/node/node-14.qa.subgroup.mycompany.com/step/1?offset=0&lastmod=0 + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=1tik7xow5yg5t1avvahzumv6u1;Path=/ + body: "\n \n 146\n 1409\n true\n true\n false\n succeeded\n\ + \ 1389894504000\n 1602\n 99.57597173144876\n 1415\n \n \ + \ \n \n \n \n" diff --git a/src/test/resources/betamax/tapes/execution_output_forstep.yaml b/src/test/resources/betamax/tapes/execution_output_forstep.yaml new file mode 100644 index 0000000..974d04e --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_output_forstep.yaml @@ -0,0 +1,24 @@ +!tape +name: execution_output_forstep +interactions: +- recorded: 2014-01-17T01:10:44.001Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/146/output/step/1?offset=0&lastmod=0 + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=37vkq48ddskqv2f3etipviu8;Path=/ + body: "\n \n 146\n 1409\n true\n true\n false\n succeeded\n\ + \ 1389894504000\n 1602\n 99.57597173144876\n 1415\n \n \n \n \n \n \n \n" diff --git a/src/test/resources/org/rundeck/api/parser/output-filtered.xml b/src/test/resources/org/rundeck/api/parser/output-filtered.xml new file mode 100644 index 0000000..9d2fb98 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/output-filtered.xml @@ -0,0 +1,19 @@ + + + 146 + 1409 + true + true + false + succeeded + 1389894504000 + 1602 + 99.9 + 1415 + + + + + + diff --git a/src/test/resources/org/rundeck/api/parser/output-state.xml b/src/test/resources/org/rundeck/api/parser/output-state.xml new file mode 100644 index 0000000..c0210d7 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/output-state.xml @@ -0,0 +1,46 @@ + + + 146 + 1409 + true + true + false + succeeded + 1389894504000 + 1602 + 99.57597173144876 + 1415 + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/org/rundeck/api/parser/output-unmodified.xml b/src/test/resources/org/rundeck/api/parser/output-unmodified.xml new file mode 100644 index 0000000..dc4fad9 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/output-unmodified.xml @@ -0,0 +1,16 @@ + + + 146 + 0 + true + true + Unmodified + true + false + succeeded + 1389894504000 + 1602 + 1415 + + + diff --git a/src/test/resources/org/rundeck/api/parser/output1.xml b/src/test/resources/org/rundeck/api/parser/output1.xml new file mode 100644 index 0000000..89f508a --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/output1.xml @@ -0,0 +1,22 @@ + + + 146 + 1409 + true + true + false + succeeded + 1389894504000 + 1602 + 99.9 + 1415 + + + + + + + From ad49b635b7b42155620a956f76377229c8aedeae Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 17:32:55 -0800 Subject: [PATCH 17/89] Remove println --- src/main/java/org/rundeck/api/parser/OutputEntryParser.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/rundeck/api/parser/OutputEntryParser.java b/src/main/java/org/rundeck/api/parser/OutputEntryParser.java index 36fbca8..c5268dc 100644 --- a/src/main/java/org/rundeck/api/parser/OutputEntryParser.java +++ b/src/main/java/org/rundeck/api/parser/OutputEntryParser.java @@ -87,7 +87,6 @@ public class OutputEntryParser implements XmlNodeParser { } try { Date parse = dateFormat.parse(s); - System.out.println(s + ": " + parse.getTime()); return parse; } catch (ParseException e) { return null; From 6b2b7954d3c56b976c6d14353e9addca4cc0ea1d Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 17:34:42 -0800 Subject: [PATCH 18/89] Add output/state sequence result for api v10 --- .../java/org/rundeck/api/RundeckClient.java | 30 +++++++++++ .../org/rundeck/api/RundeckClientTest.java | 51 +++++++++++++++++++ .../betamax/tapes/execution_output_state.yaml | 32 ++++++++++++ .../tapes/execution_output_state_only.yaml | 30 +++++++++++ 4 files changed, 143 insertions(+) create mode 100644 src/test/resources/betamax/tapes/execution_output_state.yaml create mode 100644 src/test/resources/betamax/tapes/execution_output_state_only.yaml diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 6c0d248..221f735 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -3435,6 +3435,36 @@ public class RundeckClient implements Serializable { } return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } + /** + * Get the execution state output sequence of the given job + * + * @param executionId identifier of the execution - mandatory + * @param stateOnly if true, include only state change output entries, otherwise include state and log entries + * @param offset byte offset to read from in the file. 0 indicates the beginning. + * @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 + * @param maxlines maximum number of lines to retrieve forward from the specified offset. + * @return {@link RundeckOutput} + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckOutput getJobExecutionOutputState(Long executionId, boolean stateOnly, int offset, long lastmod, int maxlines) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); + ApiPathBuilder param = new ApiPathBuilder("/execution/", executionId.toString(), "/output/state") + .param("offset", offset); + if (lastmod >= 0) { + param.param("lastmod", lastmod); + } + if (maxlines > 0) { + param.param("maxlines", maxlines); + } + if(stateOnly) { + param.param("stateOnly", true); + } + return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); + } private OutputEntryParser createOutputEntryParser() { if (getApiVersion() <= Version.V5.versionNumber) { diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index 76cac2c..5e925a5 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -1136,6 +1136,57 @@ public class RundeckClientTest { Assert.assertEquals(1, output.getLogEntries().size()); } + /** + * Execution output state sequence + */ + @Test + @Betamax(tape = "execution_output_state", mode = TapeMode.READ_ONLY) + public void executionOutputState() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckOutput output = client.getJobExecutionOutputState(146L,false,0,0L,-1); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals(null, output.getFilterNode()); + Assert.assertEquals(null, output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.57597), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(15, output.getLogEntries().size()); + } + /** + * Execution output state sequence + */ + @Test + @Betamax(tape = "execution_output_state_only", mode = TapeMode.READ_ONLY) + public void executionOutputStateOnly() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckOutput output = client.getJobExecutionOutputState(146L,true,0,0L,-1); + + Assert.assertEquals(new Long(1602), output.getExecDuration()); + Assert.assertEquals(new Long(146), output.getExecutionId()); + Assert.assertEquals(new Long(1389894504000L), output.getLastModified()); + Assert.assertEquals(null, output.getFilterNode()); + Assert.assertEquals(null, output.getFilterStep()); + Assert.assertEquals(1409, output.getOffset()); + Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, output.getStatus()); + Assert.assertEquals(new Float(99.57597), output.getPercentLoaded()); + Assert.assertEquals(1415, output.getTotalSize()); + Assert.assertEquals(true, output.isCompleted()); + Assert.assertEquals(true, output.isExecCompleted()); + Assert.assertEquals(false, output.isEmpty()); + Assert.assertEquals(false, output.isHasFailedNodes()); + Assert.assertEquals(false, output.isUnmodified()); + Assert.assertEquals(12, output.getLogEntries().size()); + } + @Before public void setUp() throws Exception { // not that you can put whatever here, because we don't actually connect to the RunDeck instance diff --git a/src/test/resources/betamax/tapes/execution_output_state.yaml b/src/test/resources/betamax/tapes/execution_output_state.yaml new file mode 100644 index 0000000..7833717 --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_output_state.yaml @@ -0,0 +1,32 @@ +!tape +name: execution_output_state +interactions: +- recorded: 2014-01-17T01:32:08.245Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/146/output/state?offset=0&lastmod=0 + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=wanbmac3kkhgo3jghy3twd7g;Path=/ + body: "\n \n 146\n 1409\n true\n true\n false\n succeeded\n\ + \ 1389894504000\n 1602\n 99.57597173144876\n 1415\n \n \n \n \n \n \n \n \n \n \n \n \n\ + \ \n \n \n \n \n \n" diff --git a/src/test/resources/betamax/tapes/execution_output_state_only.yaml b/src/test/resources/betamax/tapes/execution_output_state_only.yaml new file mode 100644 index 0000000..5667072 --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_output_state_only.yaml @@ -0,0 +1,30 @@ +!tape +name: execution_output_state_only +interactions: +- recorded: 2014-01-17T01:34:08.528Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/146/output/state?offset=0&lastmod=0&stateOnly=true + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=19nlvjuo85wqz1lwl7biksu77h;Path=/ + body: "\n \n 146\n 1409\n true\n true\n false\n succeeded\n\ + \ 1389894504000\n 1602\n 99.57597173144876\n 1415\n \n \n \n \n \n \n\ + \ \n \n \n \n \n \n \n \n \n" From d29d2067d05bdcbb76cd8781b44e1e8b0d874bba Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Thu, 16 Jan 2014 17:40:05 -0800 Subject: [PATCH 19/89] Update status for api v10 support --- src/site/confluence/status.confluence | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence index 8b64daf..5e617d2 100644 --- a/src/site/confluence/status.confluence +++ b/src/site/confluence/status.confluence @@ -88,6 +88,6 @@ h2. RunDeck API version 10 [Documentation of the RunDeck API version 10|http://rundeck.org/2.0.0/api/index.html] * Execution State - Retrieve workflow step and node state information - *TODO* -* Execution Output with State - Retrieve log output with state change information - *TODO* -* Execution Output - Retrieve log output for a particular node or step - *TODO* -* Execution Info - added successfulNodes and failedNodes detail. - *TODO* +* Execution Output with State - Retrieve log output with state change information - OK +* Execution Output - Retrieve log output for a particular node or step - OK +* Execution Info - added successfulNodes and failedNodes detail. - OK From cbdb5c818e986cbd90794b8eeff47c8592c02b04 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 10:17:27 -0800 Subject: [PATCH 20/89] WIP: add parsing for execution state --- .../org/rundeck/api/domain/BaseState.java | 47 +++++++ .../api/domain/RundeckExecutionState.java | 38 ++++++ .../api/domain/RundeckWFExecState.java | 43 ++++++ .../org/rundeck/api/domain/WorkflowState.java | 40 ++++++ .../api/domain/WorkflowStepContextState.java | 25 ++++ .../rundeck/api/domain/WorkflowStepState.java | 37 ++++++ .../rundeck/api/parser/BaseStateParser.java | 42 ++++++ .../api/parser/ExecutionStateParser.java | 60 +++++++++ .../IndexedWorkflowStepStateParser.java | 61 +++++++++ .../api/parser/WorkflowStateParser.java | 83 ++++++++++++ .../WorkflowStepContextStateParser.java | 27 ++++ .../api/parser/WorkflowStepStateParser.java | 51 ++++++++ .../api/parser/BaseStateParserTest.java | 40 ++++++ .../api/parser/ExecutionStateParserTest.java | 43 ++++++ .../IndexedWorkflowStepStateParserTest.java | 73 +++++++++++ .../api/parser/WorkflowStateParserTest.java | 65 ++++++++++ .../parser/WorkflowStepStateParserTest.java | 122 ++++++++++++++++++ .../rundeck/api/parser/execution-state1.xml | 81 ++++++++++++ .../rundeck/api/parser/execution-state2.xml | 96 ++++++++++++++ 19 files changed, 1074 insertions(+) create mode 100644 src/main/java/org/rundeck/api/domain/BaseState.java create mode 100644 src/main/java/org/rundeck/api/domain/RundeckExecutionState.java create mode 100644 src/main/java/org/rundeck/api/domain/RundeckWFExecState.java create mode 100644 src/main/java/org/rundeck/api/domain/WorkflowState.java create mode 100644 src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java create mode 100644 src/main/java/org/rundeck/api/domain/WorkflowStepState.java create mode 100644 src/main/java/org/rundeck/api/parser/BaseStateParser.java create mode 100644 src/main/java/org/rundeck/api/parser/ExecutionStateParser.java create mode 100644 src/main/java/org/rundeck/api/parser/IndexedWorkflowStepStateParser.java create mode 100644 src/main/java/org/rundeck/api/parser/WorkflowStateParser.java create mode 100644 src/main/java/org/rundeck/api/parser/WorkflowStepContextStateParser.java create mode 100644 src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java create mode 100644 src/test/java/org/rundeck/api/parser/BaseStateParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/ExecutionStateParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/IndexedWorkflowStepStateParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/WorkflowStateParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java create mode 100644 src/test/resources/org/rundeck/api/parser/execution-state1.xml create mode 100644 src/test/resources/org/rundeck/api/parser/execution-state2.xml diff --git a/src/main/java/org/rundeck/api/domain/BaseState.java b/src/main/java/org/rundeck/api/domain/BaseState.java new file mode 100644 index 0000000..7c7c146 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/BaseState.java @@ -0,0 +1,47 @@ +package org.rundeck.api.domain; + +import java.util.Date; +import java.util.List; +import java.util.Set; + +/** + * $INTERFACE is ... User: greg Date: 1/17/14 Time: 11:26 AM + */ +public class BaseState { + private Date startTime; + private Date endTime; + private Date updateTime; + private RundeckWFExecState executionState; + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getEndTime() { + return endTime; + } + + public void setEndTime(Date endTime) { + this.endTime = endTime; + } + + public RundeckWFExecState getExecutionState() { + return executionState; + } + + public void setExecutionState(RundeckWFExecState executionState) { + this.executionState = executionState; + } + + public Date getUpdateTime() { + return updateTime; + } + + public void setUpdateTime(Date updateTime) { + this.updateTime = updateTime; + } +} diff --git a/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java new file mode 100644 index 0000000..139a76d --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java @@ -0,0 +1,38 @@ +package org.rundeck.api.domain; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:41 PM + */ +public class RundeckExecutionState extends WorkflowState{ + private long executionId; + private Set allNodes; + private Map> nodeStates; + + public Set getAllNodes() { + return allNodes; + } + + public void setAllNodes(Set allNodes) { + this.allNodes = allNodes; + } + + public Map> getNodeStates() { + return nodeStates; + } + + public void setNodeStates(Map> nodeStates) { + this.nodeStates = nodeStates; + } + + public long getExecutionId() { + return executionId; + } + + public void setExecutionId(long executionId) { + this.executionId = executionId; + } +} diff --git a/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java b/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java new file mode 100644 index 0000000..f15f8ea --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java @@ -0,0 +1,43 @@ +package org.rundeck.api.domain; + +/** + * $INTERFACE is ... User: greg Date: 1/17/14 Time: 11:27 AM + */ +public enum RundeckWFExecState { + /** + * Waiting to start running + */ + WAITING, + /** + * Currently running + */ + RUNNING, + /** + * Running error handler + */ + RUNNING_HANDLER, + /** + * Finished running successfully + */ + SUCCEEDED, + /** + * Finished with a failure + */ + FAILED, + /** + * Execution was aborted + */ + ABORTED, + /** + * Partial success for some nodes + */ + NODE_PARTIAL_SUCCEEDED, + /** + * Mixed states among nodes + */ + NODE_MIXED, + /** + * After waiting the execution did not start + */ + NOT_STARTED,; +} diff --git a/src/main/java/org/rundeck/api/domain/WorkflowState.java b/src/main/java/org/rundeck/api/domain/WorkflowState.java new file mode 100644 index 0000000..753ecab --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/WorkflowState.java @@ -0,0 +1,40 @@ +package org.rundeck.api.domain; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:44 PM + */ +public class WorkflowState extends BaseState{ + private int stepCount; + private Set targetNodes; + private List steps; + + public int getStepCount() { + return stepCount; + } + + public void setStepCount(int stepCount) { + this.stepCount = stepCount; + } + + public Set getTargetNodes() { + return targetNodes; + } + + public void setTargetNodes(Set targetNodes) { + this.targetNodes = targetNodes; + } + + public List getSteps() { + return steps; + } + + public void setSteps(List steps) { + this.steps = steps; + } + +} diff --git a/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java new file mode 100644 index 0000000..3bebc05 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java @@ -0,0 +1,25 @@ +package org.rundeck.api.domain; + +/** + * A state for a particular step + */ +public class WorkflowStepContextState extends BaseState { + private String stepContextId; + private String stepNum; + + public String getStepContextId() { + return stepContextId; + } + + public void setStepContextId(String stepContextId) { + this.stepContextId = stepContextId; + } + + public String getStepNum() { + return stepNum; + } + + public void setStepNum(String stepNum) { + this.stepNum = stepNum; + } +} diff --git a/src/main/java/org/rundeck/api/domain/WorkflowStepState.java b/src/main/java/org/rundeck/api/domain/WorkflowStepState.java new file mode 100644 index 0000000..b7e6306 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/WorkflowStepState.java @@ -0,0 +1,37 @@ +package org.rundeck.api.domain; + +import java.util.List; +import java.util.Map; + +/** + * $INTERFACE is ... User: greg Date: 1/17/14 Time: 12:03 PM + */ +public class WorkflowStepState extends WorkflowStepContextState { + private boolean nodeStep; + private WorkflowState subWorkflow; + private Map nodeStates; + + public boolean isNodeStep() { + return nodeStep; + } + + public void setNodeStep(boolean nodeStep) { + this.nodeStep = nodeStep; + } + + public WorkflowState getSubWorkflow() { + return subWorkflow; + } + + public void setSubWorkflow(WorkflowState subWorkflow) { + this.subWorkflow = subWorkflow; + } + + public Map getNodeStates() { + return nodeStates; + } + + public void setNodeStates(Map nodeStates) { + this.nodeStates = nodeStates; + } +} diff --git a/src/main/java/org/rundeck/api/parser/BaseStateParser.java b/src/main/java/org/rundeck/api/parser/BaseStateParser.java new file mode 100644 index 0000000..5cd31d1 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/BaseStateParser.java @@ -0,0 +1,42 @@ +package org.rundeck.api.parser; + +import org.apache.commons.lang.StringUtils; +import org.dom4j.Node; +import org.rundeck.api.domain.BaseState; +import org.rundeck.api.domain.RundeckWFExecState; + +/** + * $INTERFACE is ... User: greg Date: 1/17/14 Time: 12:19 PM + */ +public class BaseStateParser implements XmlNodeParser { + public static void parseBaseState(Node targetNode, BaseState state) { + state.setEndTime(WorkflowStateParser.parseDate(StringUtils.trimToNull(targetNode.valueOf("endTime")))); + state.setStartTime(WorkflowStateParser.parseDate(StringUtils.trimToNull(targetNode.valueOf("startTime")))); + state.setUpdateTime(WorkflowStateParser.parseDate(StringUtils.trimToNull(targetNode.valueOf("updateTime")))); + + try { + state.setExecutionState(RundeckWFExecState.valueOf(StringUtils.upperCase(targetNode.valueOf + ("executionState")))); + } catch (IllegalArgumentException e) { + state.setExecutionState(null); + } + } + + private String xpath; + + public BaseStateParser() { + } + + public BaseStateParser(String xpath) { + + this.xpath = xpath; + } + + @Override + public BaseState parseXmlNode(Node node) { + Node targetNode = xpath != null ? node.selectSingleNode(xpath) : node; + BaseState baseState = new BaseState(); + parseBaseState(targetNode, baseState); + return baseState; + } +} diff --git a/src/main/java/org/rundeck/api/parser/ExecutionStateParser.java b/src/main/java/org/rundeck/api/parser/ExecutionStateParser.java new file mode 100644 index 0000000..180b425 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/ExecutionStateParser.java @@ -0,0 +1,60 @@ +package org.rundeck.api.parser; + +import org.apache.commons.lang.StringUtils; +import org.dom4j.Node; +import org.rundeck.api.domain.*; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:42 PM + */ +public class ExecutionStateParser implements XmlNodeParser { + private String xpath; + + public ExecutionStateParser() { + super(); + } + + /** + * @param xpath of the execution element if it is not the root node + */ + public ExecutionStateParser(String xpath) { + this(); + this.xpath = xpath; + } + + @Override + public RundeckExecutionState parseXmlNode(Node node) { + Node targetNode = xpath != null ? node.selectSingleNode(xpath) : node; + RundeckExecutionState rundeckExecutionState = new RundeckExecutionState(); + rundeckExecutionState.setExecutionId(Long.valueOf(targetNode.valueOf("@id"))); + + WorkflowStateParser.parseWorkflowState(targetNode, rundeckExecutionState); + + + final List rundeckNodes = + new ListParser(new NodeParser(), "allNodes/nodes/node").parseXmlNode(targetNode); + rundeckExecutionState.setAllNodes(new HashSet(rundeckNodes)); + + + //node states + HashMap> nodeStates = new HashMap>(); + + for (Object o : targetNode.selectNodes("nodes/node")) { + final Node nodeStateNode = (Node) o; + final String nodeName = StringUtils.trimToNull(nodeStateNode.valueOf("@name")); + if (null != nodeName) { + ListParser workflowStepStateListParser + = new ListParser(new IndexedWorkflowStepStateParser(rundeckExecutionState, nodeName) + , "steps/step"); + nodeStates.put(nodeName, workflowStepStateListParser.parseXmlNode(nodeStateNode)); + } + } + rundeckExecutionState.setNodeStates(nodeStates); + + return rundeckExecutionState; + } +} diff --git a/src/main/java/org/rundeck/api/parser/IndexedWorkflowStepStateParser.java b/src/main/java/org/rundeck/api/parser/IndexedWorkflowStepStateParser.java new file mode 100644 index 0000000..16f7fd4 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/IndexedWorkflowStepStateParser.java @@ -0,0 +1,61 @@ +package org.rundeck.api.parser; + +import org.apache.commons.lang.StringUtils; +import org.dom4j.Node; +import org.rundeck.api.domain.WorkflowState; +import org.rundeck.api.domain.WorkflowStepContextState; +import org.rundeck.api.domain.WorkflowStepState; + +/** + * Returns a WorkflowStepContextState by looking up the given Rundeck node's state in the workflow, using the step + * context path of the "stepctx" element of the selected DOM node. + */ +public class IndexedWorkflowStepStateParser implements XmlNodeParser { + private final WorkflowState workflowState; + private String rundeckNodeName; + + @Override + public WorkflowStepContextState parseXmlNode(final Node node) { + //look for workflow step state based on node name and stepctx found on the node + final String stepctx = StringUtils.trimToNull(node.valueOf("stepctx")); + final WorkflowStepState foundStep = lookupContext(stepctx, workflowState); + //look up node state for this node + if (null != foundStep + && null != foundStep.getNodeStates() + && null != foundStep.getNodeStates().get(rundeckNodeName)) { + return foundStep.getNodeStates().get(rundeckNodeName); + } + + + return null; + } + + /** + * look up the workflow step state for the step context, from the root workflow + * + * @param stepctx + * @param initial + * + * @return + */ + public static WorkflowStepState lookupContext(final String stepctx, final WorkflowState initial) { + final String[] parts = stepctx.split("/"); + //descend workflow steps to find correct step + WorkflowState current = initial; + WorkflowStepState currentStep = null; + for (int i = 0; i < parts.length; i++) { + final String part = parts[i]; + final WorkflowStepState workflowStepState = current.getSteps().get(Integer.parseInt(part) - 1); + currentStep = workflowStepState; + if (i < parts.length - 1) { + current = currentStep.getSubWorkflow(); + } + } + return currentStep; + } + + public IndexedWorkflowStepStateParser(final WorkflowState workflowState, final String rundeckNodeName) { + this.workflowState = workflowState; + this.rundeckNodeName = rundeckNodeName; + } +} diff --git a/src/main/java/org/rundeck/api/parser/WorkflowStateParser.java b/src/main/java/org/rundeck/api/parser/WorkflowStateParser.java new file mode 100644 index 0000000..36ffe57 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/WorkflowStateParser.java @@ -0,0 +1,83 @@ +package org.rundeck.api.parser; + +import org.apache.commons.lang.StringUtils; +import org.dom4j.Node; +import org.rundeck.api.domain.*; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:44 PM + */ +public class WorkflowStateParser implements XmlNodeParser { + private String xpath; + + public WorkflowStateParser() { + } + + public WorkflowStateParser(String xpath) { + this(); + this.xpath = xpath; + } + + private static final ThreadLocal w3cDateFormat = new ThreadLocal() { + protected DateFormat initialValue() { + SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + fmt.setTimeZone(TimeZone.getTimeZone("GMT")); + return fmt; + } + }; + public static Date parseDate(String s) { + if (null == s) { + return null; + } + try { + Date parse = w3cDateFormat.get().parse(s); + return parse; + } catch (ParseException e) { + return null; + } + } + + private static int integerValue(final String value, final int defValue) { + int parseMax = defValue; + try { + parseMax = null != value ? Integer.parseInt(value) : defValue; + } catch (NumberFormatException e) { + } + return parseMax; + } + + @Override + public WorkflowState parseXmlNode(Node node) { + Node targetNode = xpath != null ? node.selectSingleNode(xpath) : node; + WorkflowState state = new WorkflowState(); + parseWorkflowState(targetNode, state); + + + return state; + } + + /** + * Parse the workflow state components from the given dom node + * @param targetNode + * @param state + */ + public static void parseWorkflowState(Node targetNode, WorkflowState state) { + BaseStateParser.parseBaseState(targetNode, state); + + state.setStepCount(integerValue(StringUtils.trimToNull(targetNode.valueOf("stepCount")), 0)); + + final List rundeckNodes = + new ListParser(new NodeParser(), "targetNodes/nodes/node").parseXmlNode(targetNode); + state.setTargetNodes(new HashSet(rundeckNodes)); + + //steps + state.setSteps(new ListParser(new WorkflowStepStateParser(), + "steps/step").parseXmlNode(targetNode)); + } + +} diff --git a/src/main/java/org/rundeck/api/parser/WorkflowStepContextStateParser.java b/src/main/java/org/rundeck/api/parser/WorkflowStepContextStateParser.java new file mode 100644 index 0000000..c744cac --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/WorkflowStepContextStateParser.java @@ -0,0 +1,27 @@ +package org.rundeck.api.parser; + +import org.dom4j.Node; +import org.rundeck.api.domain.WorkflowStepContextState; +import org.rundeck.api.domain.WorkflowStepState; + +/** + * $INTERFACE is ... User: greg Date: 1/17/14 Time: 12:39 PM + */ +public class WorkflowStepContextStateParser implements XmlNodeParser { + WorkflowStepContextState inherit; + + public WorkflowStepContextStateParser(WorkflowStepContextState inherit) { + this.inherit = inherit; + } + + @Override + public WorkflowStepContextState parseXmlNode(Node node) { + WorkflowStepContextState workflowStepState = new WorkflowStepContextState(); + if(null!=inherit) { + workflowStepState.setStepNum(inherit.getStepNum()); + workflowStepState.setStepContextId(inherit.getStepContextId()); + } + BaseStateParser.parseBaseState(node, workflowStepState); + return workflowStepState; + } +} diff --git a/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java b/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java new file mode 100644 index 0000000..ede8d7c --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java @@ -0,0 +1,51 @@ +package org.rundeck.api.parser; + +import org.apache.commons.lang.StringUtils; +import org.dom4j.Node; +import org.rundeck.api.domain.BaseState; +import org.rundeck.api.domain.WorkflowStepContextState; +import org.rundeck.api.domain.WorkflowStepState; + +import java.util.HashMap; + +/** + * $INTERFACE is ... User: greg Date: 1/17/14 Time: 12:09 PM + */ +public class WorkflowStepStateParser implements XmlNodeParser { + private String xpath; + + public WorkflowStepStateParser(final String xpath) { + this.xpath = xpath; + } + + public WorkflowStepStateParser() { + } + + @Override + public WorkflowStepState parseXmlNode(final Node node) { + final Node targetNode = xpath != null ? node.selectSingleNode(xpath) : node; + final WorkflowStepState state = new WorkflowStepState(); + + BaseStateParser.parseBaseState(targetNode, state); + state.setStepContextId(StringUtils.trimToNull(targetNode.valueOf("@stepctx"))); + state.setStepNum(StringUtils.trimToNull(targetNode.valueOf("@id"))); + state.setNodeStep(Boolean.valueOf(StringUtils.trimToNull(targetNode.valueOf("nodeStep")))); + if (Boolean.valueOf(StringUtils.trimToNull(targetNode.valueOf("hasSubworkflow")))) { + //parse sub workflow + state.setSubWorkflow(new WorkflowStateParser("workflow").parseXmlNode(targetNode)); + } + if (Boolean.valueOf(StringUtils.trimToNull(targetNode.valueOf("nodeStep")))) { + //node states + final HashMap nodeStates = new HashMap(); + for (final Object o : targetNode.selectNodes("nodeStates/nodeState")) { + final Node nodeStateNode = (Node) o; + final String nodeName = StringUtils.trimToNull(nodeStateNode.valueOf("@name")); + if (null != nodeName) { + nodeStates.put(nodeName, new WorkflowStepContextStateParser(state).parseXmlNode(nodeStateNode)); + } + } + state.setNodeStates(nodeStates); + } + return state; + } +} diff --git a/src/test/java/org/rundeck/api/parser/BaseStateParserTest.java b/src/test/java/org/rundeck/api/parser/BaseStateParserTest.java new file mode 100644 index 0000000..3c17f27 --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/BaseStateParserTest.java @@ -0,0 +1,40 @@ +package org.rundeck.api.parser; + +import junit.framework.Assert; +import org.dom4j.Document; +import org.junit.Test; +import org.rundeck.api.domain.BaseState; +import org.rundeck.api.domain.RundeckWFExecState; + +import java.io.InputStream; +import java.util.Date; + +/** + * $INTERFACE is ... User: greg Date: 1/18/14 Time: 8:33 AM + */ +public class BaseStateParserTest { + @Test + public void testBase1(){ + InputStream input = getClass().getResourceAsStream("execution-state1.xml"); + Document document = ParserHelper.loadDocument(input); + BaseState baseState = new BaseState(); + BaseStateParser.parseBaseState(document.selectSingleNode("/result/executionState"), baseState); + + Assert.assertEquals(1390066160000L, baseState.getEndTime().getTime()); + Assert.assertEquals(1390066159000L, baseState.getStartTime().getTime()); + Assert.assertEquals(1390066160000L, baseState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, baseState.getExecutionState()); + } + @Test + public void testBase2(){ + InputStream input = getClass().getResourceAsStream("execution-state1.xml"); + Document document = ParserHelper.loadDocument(input); + BaseState baseState = new BaseState(); + BaseStateParser.parseBaseState(document.selectSingleNode("/result/executionState/steps/step[1]"), baseState); + + Assert.assertEquals(1390066159000L, baseState.getStartTime().getTime()); + Assert.assertEquals(1390066160000L, baseState.getEndTime().getTime()); + Assert.assertEquals(1390066160000L, baseState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, baseState.getExecutionState()); + } +} diff --git a/src/test/java/org/rundeck/api/parser/ExecutionStateParserTest.java b/src/test/java/org/rundeck/api/parser/ExecutionStateParserTest.java new file mode 100644 index 0000000..c8170bd --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/ExecutionStateParserTest.java @@ -0,0 +1,43 @@ +package org.rundeck.api.parser; + +import junit.framework.Assert; +import org.dom4j.Document; +import org.junit.Test; +import org.rundeck.api.domain.RundeckExecutionState; +import org.rundeck.api.domain.RundeckNodeIdentity; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:42 PM + */ +public class ExecutionStateParserTest { + @Test + public void testBasic(){ + InputStream input = getClass().getResourceAsStream("execution-state1.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckExecutionState execution = new ExecutionStateParser("/result/executionState").parseXmlNode + (document); + + Assert.assertEquals(149L, execution.getExecutionId()); + + HashSet expectedTargetNodes = new HashSet(Arrays.asList( + "node-111.qa.subgroup.mycompany.com", + "node-14.qa.subgroup.mycompany.com", + "node-6.qa.subgroup.mycompany.com" + )); + + Assert.assertEquals(3, execution.getAllNodes().size()); + for (RundeckNodeIdentity rundeckNodeIdentity : execution.getAllNodes()) { + Assert.assertTrue(expectedTargetNodes.contains(rundeckNodeIdentity.getName())); + } + + Assert.assertEquals(3,execution.getNodeStates().size()); + for (String s : execution.getNodeStates().keySet()) { + Assert.assertTrue(expectedTargetNodes.contains(s)); + } + } +} diff --git a/src/test/java/org/rundeck/api/parser/IndexedWorkflowStepStateParserTest.java b/src/test/java/org/rundeck/api/parser/IndexedWorkflowStepStateParserTest.java new file mode 100644 index 0000000..7b865c8 --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/IndexedWorkflowStepStateParserTest.java @@ -0,0 +1,73 @@ +package org.rundeck.api.parser; + +import junit.framework.Assert; +import org.dom4j.Document; +import org.dom4j.DocumentException; +import org.dom4j.DocumentHelper; +import org.dom4j.Element; +import org.junit.Test; +import org.rundeck.api.domain.WorkflowState; +import org.rundeck.api.domain.WorkflowStepContextState; +import org.rundeck.api.domain.WorkflowStepState; + +import java.util.Arrays; +import java.util.HashMap; + +/** + * $INTERFACE is ... User: greg Date: 1/18/14 Time: 9:57 AM + */ +public class IndexedWorkflowStepStateParserTest { + @Test + public void testLookupContextSimple1(){ + WorkflowState workflowState = new WorkflowState(); + WorkflowStepState step1 = new WorkflowStepState(); + workflowState.setSteps(Arrays.asList(step1)); + WorkflowStepState stepState = IndexedWorkflowStepStateParser.lookupContext("1", workflowState); + Assert.assertEquals(step1,stepState); + } + @Test + public void testLookupContextSimple2(){ + WorkflowState workflowState = new WorkflowState(); + WorkflowStepState step1 = new WorkflowStepState(); + WorkflowStepState step2 = new WorkflowStepState(); + workflowState.setSteps(Arrays.asList(step1,step2)); + WorkflowStepState stepState = IndexedWorkflowStepStateParser.lookupContext("2", workflowState); + Assert.assertEquals(step2,stepState); + } + @Test + public void testLookupContextDescend1(){ + WorkflowState workflowState = new WorkflowState(); + WorkflowStepState step1 = new WorkflowStepState(); + WorkflowStepState step2 = new WorkflowStepState(); + WorkflowState sub1 = new WorkflowState(); + step2.setSubWorkflow(sub1); + workflowState.setSteps(Arrays.asList(step1,step2)); + + WorkflowStepState step21 = new WorkflowStepState(); + sub1.setSteps(Arrays.asList(step21)); + + WorkflowStepState stepState = IndexedWorkflowStepStateParser.lookupContext("2/1", workflowState); + Assert.assertEquals(step21,stepState); + } + @Test + public void testParse1() throws DocumentException { + WorkflowState workflowState = new WorkflowState(); + WorkflowStepState step1 = new WorkflowStepState(); + WorkflowStepState step2 = new WorkflowStepState(); + WorkflowState sub1 = new WorkflowState(); + step2.setSubWorkflow(sub1); + workflowState.setSteps(Arrays.asList(step1,step2)); + + WorkflowStepState step21 = new WorkflowStepState(); + sub1.setSteps(Arrays.asList(step21)); + HashMap nodeStates = new HashMap(); + WorkflowStepContextState nodeState1 = new WorkflowStepContextState(); + nodeStates.put("dignan", nodeState1); + step21.setNodeStates(nodeStates); + + Document document = DocumentHelper.parseText("2/1"); + + WorkflowStepContextState result = new IndexedWorkflowStepStateParser(workflowState,"dignan").parseXmlNode(document); + Assert.assertEquals(nodeState1,result); + } +} diff --git a/src/test/java/org/rundeck/api/parser/WorkflowStateParserTest.java b/src/test/java/org/rundeck/api/parser/WorkflowStateParserTest.java new file mode 100644 index 0000000..3d90d9e --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/WorkflowStateParserTest.java @@ -0,0 +1,65 @@ +package org.rundeck.api.parser; + +import junit.framework.Assert; +import org.dom4j.Document; +import org.junit.Test; +import org.rundeck.api.domain.*; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; + +/** + * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:47 PM + */ +public class WorkflowStateParserTest { + @Test + public void parseBasic(){ + InputStream input = getClass().getResourceAsStream("execution-state1.xml"); + Document document = ParserHelper.loadDocument(input); + + WorkflowState execution = new WorkflowStateParser("result/executionState").parseXmlNode(document); + Assert.assertEquals(1390066159000L, execution.getStartTime().getTime()); + Assert.assertEquals(1390066160000L, execution.getEndTime().getTime()); + Assert.assertEquals(1390066160000L, execution.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, execution.getExecutionState()); + Assert.assertEquals(1, execution.getStepCount()); + Assert.assertEquals(3, execution.getTargetNodes().size()); + HashSet expectedTargetNodes = new HashSet(Arrays.asList( + "node-111.qa.subgroup.mycompany.com", + "node-14.qa.subgroup.mycompany.com", + "node-6.qa.subgroup.mycompany.com" + )); + for (RundeckNodeIdentity rundeckNodeIdentity : execution.getTargetNodes()) { + Assert.assertTrue(expectedTargetNodes.contains(rundeckNodeIdentity.getName())); + } + + // + Assert.assertEquals(1,execution.getSteps().size()); + WorkflowStepState step1 = execution.getSteps().get(0); + } + @Test + public void parse(){ + InputStream input = getClass().getResourceAsStream("execution-state2.xml"); + Document document = ParserHelper.loadDocument(input); + + WorkflowState execution = new WorkflowStateParser("result/executionState").parseXmlNode(document); + Assert.assertEquals(1390066061000L, execution.getStartTime().getTime()); + Assert.assertEquals(null, execution.getEndTime()); + Assert.assertEquals(1390066067000L, execution.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.RUNNING, execution.getExecutionState()); + Assert.assertEquals(2, execution.getStepCount()); + Assert.assertEquals(1, execution.getTargetNodes().size()); + HashSet expectedTargetNodes = new HashSet(Arrays.asList( + "dignan" + )); + for (RundeckNodeIdentity rundeckNodeIdentity : execution.getTargetNodes()) { + Assert.assertTrue(expectedTargetNodes.contains(rundeckNodeIdentity.getName())); + } + + // + Assert.assertEquals(2,execution.getSteps().size()); + WorkflowStepState step1 = execution.getSteps().get(0); + } +} diff --git a/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java b/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java new file mode 100644 index 0000000..418ecdb --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java @@ -0,0 +1,122 @@ +package org.rundeck.api.parser; + +import junit.framework.Assert; +import org.dom4j.Document; +import org.junit.Test; +import org.rundeck.api.domain.*; + +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashSet; + +/** + * $INTERFACE is ... User: greg Date: 1/18/14 Time: 9:00 AM + */ +public class WorkflowStepStateParserTest { + + @Test + public void testParse1() { + InputStream input = getClass().getResourceAsStream("execution-state1.xml"); + Document document = ParserHelper.loadDocument(input); + WorkflowStepState stepState = new WorkflowStepStateParser().parseXmlNode(document.selectSingleNode + ("/result/executionState/steps/step[1]")); + + Assert.assertNotNull(stepState); + Assert.assertEquals(true, stepState.isNodeStep()); + Assert.assertEquals(null, stepState.getSubWorkflow()); + Assert.assertNotNull(stepState.getNodeStates()); + Assert.assertEquals("1", stepState.getStepContextId()); + Assert.assertEquals("1", stepState.getStepNum()); + Assert.assertEquals(1390066159000L, stepState.getStartTime().getTime()); + Assert.assertEquals(1390066160000L, stepState.getEndTime().getTime()); + Assert.assertEquals(1390066160000L, stepState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, stepState.getExecutionState()); + HashSet expectedTargetNodes = new HashSet(Arrays.asList( + "node-111.qa.subgroup.mycompany.com", + "node-14.qa.subgroup.mycompany.com", + "node-6.qa.subgroup.mycompany.com" + )); + + int i = 0; + for (String s : stepState.getNodeStates().keySet()) { + Assert.assertTrue(expectedTargetNodes.contains(s)); + WorkflowStepContextState workflowStepContextState = stepState.getNodeStates().get(s); + Assert.assertEquals("1", workflowStepContextState.getStepContextId()); + Assert.assertEquals("1", workflowStepContextState.getStepNum()); + Assert.assertEquals(1390066159000L + (i * 1000), workflowStepContextState.getStartTime().getTime()); + Assert.assertEquals(1390066159000L + (i * 1000), workflowStepContextState.getEndTime().getTime()); + Assert.assertEquals(1390066159000L + (i * 1000), workflowStepContextState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, workflowStepContextState.getExecutionState()); + i++; + } + + } + @Test + public void testParseRunning1() { + InputStream input = getClass().getResourceAsStream("execution-state2.xml"); + Document document = ParserHelper.loadDocument(input); + + WorkflowStepState stepState = new WorkflowStepStateParser().parseXmlNode(document.selectSingleNode + ("/result/executionState/steps/step[1]")); + + Assert.assertNotNull(stepState); + Assert.assertEquals(true, stepState.isNodeStep()); + Assert.assertEquals(null, stepState.getSubWorkflow()); + Assert.assertEquals("1", stepState.getStepContextId()); + Assert.assertEquals("1", stepState.getStepNum()); + Assert.assertNotNull(stepState.getNodeStates()); + Assert.assertEquals(1390066061000L, stepState.getStartTime().getTime()); + Assert.assertEquals(1390066066000L, stepState.getEndTime().getTime()); + Assert.assertEquals(1390066061000L, stepState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, stepState.getExecutionState()); + HashSet expectedTargetNodes = new HashSet(Arrays.asList( + "dignan" + )); + + WorkflowStepContextState workflowStepContextState = stepState.getNodeStates().get("dignan"); + Assert.assertEquals("1", workflowStepContextState.getStepContextId()); + Assert.assertEquals("1", workflowStepContextState.getStepNum()); + Assert.assertEquals(1390066061000L, workflowStepContextState.getStartTime().getTime()); + Assert.assertEquals(1390066066000L, workflowStepContextState.getEndTime().getTime()); + Assert.assertEquals(1390066066000L, workflowStepContextState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED, workflowStepContextState.getExecutionState()); + + } + @Test + public void testParseRunning2() { + InputStream input = getClass().getResourceAsStream("execution-state2.xml"); + Document document = ParserHelper.loadDocument(input); + + WorkflowStepState stepState = new WorkflowStepStateParser().parseXmlNode(document.selectSingleNode + ("/result/executionState/steps/step[2]")); + + Assert.assertNotNull(stepState); + Assert.assertEquals(false, stepState.isNodeStep()); + Assert.assertNotNull(stepState.getSubWorkflow()); + Assert.assertNull(stepState.getNodeStates()); + Assert.assertEquals("2", stepState.getStepContextId()); + Assert.assertEquals("2",stepState.getStepNum()); + Assert.assertEquals(1390066066000L, stepState.getStartTime().getTime()); + Assert.assertNull(stepState.getEndTime()); + Assert.assertEquals(1390066066000L, stepState.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.RUNNING, stepState.getExecutionState()); + + + //sub workflow + WorkflowState subWorkflow = stepState.getSubWorkflow(); + Assert.assertEquals(1,subWorkflow.getSteps().size()); + Assert.assertEquals(1,subWorkflow.getTargetNodes().size()); + + WorkflowStepState stepState1 = subWorkflow.getSteps().get(0); + Assert.assertEquals(true, stepState1.isNodeStep()); + Assert.assertNull(stepState1.getSubWorkflow()); + Assert.assertNotNull(stepState1.getNodeStates()); + Assert.assertEquals("2/1", stepState1.getStepContextId()); + Assert.assertEquals("1", stepState1.getStepNum()); + Assert.assertEquals(1390066067000L, stepState1.getStartTime().getTime()); + Assert.assertNull(stepState1.getEndTime()); + Assert.assertEquals(1390066067000L, stepState1.getUpdateTime().getTime()); + Assert.assertEquals(RundeckWFExecState.RUNNING, stepState1.getExecutionState()); + + } +} diff --git a/src/test/resources/org/rundeck/api/parser/execution-state1.xml b/src/test/resources/org/rundeck/api/parser/execution-state1.xml new file mode 100644 index 0000000..71c47af --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/execution-state1.xml @@ -0,0 +1,81 @@ + + + 149 + dignan + SUCCEEDED + true + + + + + + + + + + + + + + + 1 + 2014-01-18T17:29:20Z + 2014-01-18T17:29:19Z + 2014-01-18T17:29:20Z + + + true + SUCCEEDED + 2014-01-18T17:29:19Z + 2014-01-18T17:29:20Z + 2014-01-18T17:29:20Z + + + SUCCEEDED + 2014-01-18T17:29:19Z + 2014-01-18T17:29:19Z + 2014-01-18T17:29:19Z + + + SUCCEEDED + 2014-01-18T17:29:20Z + 2014-01-18T17:29:20Z + 2014-01-18T17:29:20Z + + + SUCCEEDED + 2014-01-18T17:29:21Z + 2014-01-18T17:29:21Z + 2014-01-18T17:29:21Z + + + + + + + + + 1 + SUCCEEDED + + + + + + + 1 + SUCCEEDED + + + + + + + 1 + SUCCEEDED + + + + + + diff --git a/src/test/resources/org/rundeck/api/parser/execution-state2.xml b/src/test/resources/org/rundeck/api/parser/execution-state2.xml new file mode 100644 index 0000000..f867aba --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/execution-state2.xml @@ -0,0 +1,96 @@ + + + 148 + dignan + RUNNING + false + + + + + + + + + + + 2 + 2014-01-18T17:27:47Z + 2014-01-18T17:27:41Z + + + + true + SUCCEEDED + 2014-01-18T17:27:41Z + 2014-01-18T17:27:41Z + 2014-01-18T17:27:46Z + + + SUCCEEDED + 2014-01-18T17:27:41Z + 2014-01-18T17:27:46Z + 2014-01-18T17:27:46Z + + + + + true + + RUNNING + false + + + + + + + + + + + 1 + 2014-01-18T17:27:47Z + 2014-01-18T17:27:47Z + + + + true + RUNNING + 2014-01-18T17:27:47Z + 2014-01-18T17:27:47Z + + + + RUNNING + 2014-01-18T17:27:47Z + 2014-01-18T17:27:47Z + + + + + + + false + RUNNING + 2014-01-18T17:27:46Z + 2014-01-18T17:27:46Z + + + + + + + + 1 + SUCCEEDED + + + 2/1 + RUNNING + + + + + + From ba136cfc4486c55cfd0b52e7b127514691e17893 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 10:27:04 -0800 Subject: [PATCH 21/89] WIP: add client interface to execution state endpoint --- .../java/org/rundeck/api/RundeckClient.java | 19 +++++++++++++++++ .../org/rundeck/api/RundeckClientTest.java | 16 ++++++++++++++ .../betamax/tapes/execution_state.yaml | 21 +++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 src/test/resources/betamax/tapes/execution_state.yaml diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 221f735..aa54b7b 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -3281,6 +3281,25 @@ public class RundeckClient implements Serializable { return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser())); } + /** + * Get the execution state of the given execution + * + * @param executionId identifier of the execution - mandatory + * @return {@link RundeckExecutionState} the execution state + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckExecutionState getExecutionState(Long executionId) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + AssertUtil.notNull(executionId, "executionId is mandatory to get the state of an execution!"); + ApiPathBuilder param = new ApiPathBuilder( + "/execution/", executionId.toString(), + "/state"); + + return new ApiCall(this).get(param, new ExecutionStateParser("result/executionState")); + } /** * Get the execution output of the given execution on the specified node diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index 5e925a5..afc90d4 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -1186,6 +1186,22 @@ public class RundeckClientTest { Assert.assertEquals(false, output.isUnmodified()); Assert.assertEquals(12, output.getLogEntries().size()); } + /** + * Execution state structure + */ + @Test + @Betamax(tape = "execution_state", mode = TapeMode.READ_ONLY) + public void executionState() throws Exception { + final RundeckClient client = createClient(TEST_TOKEN_6, 10); + RundeckExecutionState output = client.getExecutionState(149L); + + Assert.assertEquals(149,output.getExecutionId()); + Assert.assertEquals(3,output.getTargetNodes().size()); + Assert.assertEquals(3,output.getAllNodes().size()); + Assert.assertEquals(1,output.getStepCount()); + Assert.assertEquals(1,output.getSteps().size()); + Assert.assertEquals(RundeckWFExecState.SUCCEEDED,output.getExecutionState()); + } @Before public void setUp() throws Exception { diff --git a/src/test/resources/betamax/tapes/execution_state.yaml b/src/test/resources/betamax/tapes/execution_state.yaml new file mode 100644 index 0000000..35c721f --- /dev/null +++ b/src/test/resources/betamax/tapes/execution_state.yaml @@ -0,0 +1,21 @@ +!tape +name: execution_state +interactions: +- recorded: 2014-01-18T18:24:49.806Z + request: + method: GET + uri: http://rundeck.local:4440/api/10/execution/149/state + headers: + Accept: text/xml + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 10 + X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn + response: + status: 200 + headers: + Content-Type: text/xml;charset=UTF-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(7.6.0.v20120127) + Set-Cookie: JSESSIONID=x0rnzzfb3xi410pethl5ttvjb;Path=/ + body: 2014-01-18T17:29:19Z12014-01-18T17:29:20Z149dignan2014-01-18T17:29:20ZSUCCEEDEDtrue2014-01-18T17:29:19Ztrue2014-01-18T17:29:20Z2014-01-18T17:29:20ZSUCCEEDED2014-01-18T17:29:19Z2014-01-18T17:29:20Z2014-01-18T17:29:20ZSUCCEEDED2014-01-18T17:29:20Z2014-01-18T17:29:20Z2014-01-18T17:29:20ZSUCCEEDED2014-01-18T17:29:20Z2014-01-18T17:29:20Z2014-01-18T17:29:20ZSUCCEEDED1SUCCEEDED1SUCCEEDED1SUCCEEDED From 6c1f93af49c153e5ac3ed86a9c5e2aedc3478075 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 10:33:12 -0800 Subject: [PATCH 22/89] Update javadoc for workflow state domain classes --- .../java/org/rundeck/api/domain/BaseState.java | 18 +++++++++++++++++- .../api/domain/RundeckExecutionState.java | 14 +++++++++++++- .../rundeck/api/domain/RundeckWFExecState.java | 2 +- .../org/rundeck/api/domain/WorkflowState.java | 16 ++++++++++++++-- .../api/domain/WorkflowStepContextState.java | 8 ++++++++ .../rundeck/api/domain/WorkflowStepState.java | 14 +++++++++++++- 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/rundeck/api/domain/BaseState.java b/src/main/java/org/rundeck/api/domain/BaseState.java index 7c7c146..f97fa23 100644 --- a/src/main/java/org/rundeck/api/domain/BaseState.java +++ b/src/main/java/org/rundeck/api/domain/BaseState.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Set; /** - * $INTERFACE is ... User: greg Date: 1/17/14 Time: 11:26 AM + * Base execution status for a step */ public class BaseState { private Date startTime; @@ -13,6 +13,10 @@ public class BaseState { private Date updateTime; private RundeckWFExecState executionState; + /** + * Time that the execution of this step started + * @return + */ public Date getStartTime() { return startTime; } @@ -21,6 +25,10 @@ public class BaseState { this.startTime = startTime; } + /** + * Time that the execution of this step finished, or null if it has not completed + * @return + */ public Date getEndTime() { return endTime; } @@ -29,6 +37,10 @@ public class BaseState { this.endTime = endTime; } + /** + * Current state of the execution + * @return + */ public RundeckWFExecState getExecutionState() { return executionState; } @@ -37,6 +49,10 @@ public class BaseState { this.executionState = executionState; } + /** + * Time that this state was last updated + * @return + */ public Date getUpdateTime() { return updateTime; } diff --git a/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java index 139a76d..518a1d8 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java +++ b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java @@ -5,13 +5,17 @@ import java.util.Map; import java.util.Set; /** - * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:41 PM + * The state of an Execution */ public class RundeckExecutionState extends WorkflowState{ private long executionId; private Set allNodes; private Map> nodeStates; + /** + * Return the set of all rundeck nodes targetted in this execution + * @return + */ public Set getAllNodes() { return allNodes; } @@ -20,6 +24,10 @@ public class RundeckExecutionState extends WorkflowState{ this.allNodes = allNodes; } + /** + * Return the map of node name to step state list + * @return + */ public Map> getNodeStates() { return nodeStates; } @@ -28,6 +36,10 @@ public class RundeckExecutionState extends WorkflowState{ this.nodeStates = nodeStates; } + /** + * Return this execution's ID + * @return + */ public long getExecutionId() { return executionId; } diff --git a/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java b/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java index f15f8ea..86b0466 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java +++ b/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java @@ -1,7 +1,7 @@ package org.rundeck.api.domain; /** - * $INTERFACE is ... User: greg Date: 1/17/14 Time: 11:27 AM + * An execution state for a workflow or node step */ public enum RundeckWFExecState { /** diff --git a/src/main/java/org/rundeck/api/domain/WorkflowState.java b/src/main/java/org/rundeck/api/domain/WorkflowState.java index 753ecab..3ce17a2 100644 --- a/src/main/java/org/rundeck/api/domain/WorkflowState.java +++ b/src/main/java/org/rundeck/api/domain/WorkflowState.java @@ -6,13 +6,17 @@ import java.util.Map; import java.util.Set; /** - * $INTERFACE is ... User: greg Date: 1/16/14 Time: 5:44 PM + * Represents the state of a workflow of steps */ -public class WorkflowState extends BaseState{ +public class WorkflowState extends BaseState { private int stepCount; private Set targetNodes; private List steps; + /** + * Return the number of steps in this workflow + * @return + */ public int getStepCount() { return stepCount; } @@ -21,6 +25,10 @@ public class WorkflowState extends BaseState{ this.stepCount = stepCount; } + /** + * Identify the target nodes of this workflow + * @return + */ public Set getTargetNodes() { return targetNodes; } @@ -29,6 +37,10 @@ public class WorkflowState extends BaseState{ this.targetNodes = targetNodes; } + /** + * Return the list of steps for this workflow + * @return + */ public List getSteps() { return steps; } diff --git a/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java index 3bebc05..e54ab7d 100644 --- a/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java +++ b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java @@ -7,6 +7,10 @@ public class WorkflowStepContextState extends BaseState { private String stepContextId; private String stepNum; + /** + * The context id for the step in the form "#[/#[/#[...]]]" where "#" is a number + * @return + */ public String getStepContextId() { return stepContextId; } @@ -15,6 +19,10 @@ public class WorkflowStepContextState extends BaseState { this.stepContextId = stepContextId; } + /** + * The step number of this step in the current workflow, 1 indexed. + * @return + */ public String getStepNum() { return stepNum; } diff --git a/src/main/java/org/rundeck/api/domain/WorkflowStepState.java b/src/main/java/org/rundeck/api/domain/WorkflowStepState.java index b7e6306..49a8d93 100644 --- a/src/main/java/org/rundeck/api/domain/WorkflowStepState.java +++ b/src/main/java/org/rundeck/api/domain/WorkflowStepState.java @@ -4,13 +4,17 @@ import java.util.List; import java.util.Map; /** - * $INTERFACE is ... User: greg Date: 1/17/14 Time: 12:03 PM + * Represents the state of a step in a workflow */ public class WorkflowStepState extends WorkflowStepContextState { private boolean nodeStep; private WorkflowState subWorkflow; private Map nodeStates; + /** + * Return true if this step runs on each target node + * @return + */ public boolean isNodeStep() { return nodeStep; } @@ -19,6 +23,10 @@ public class WorkflowStepState extends WorkflowStepContextState { this.nodeStep = nodeStep; } + /** + * Return sub workflow if this step has one + * @return + */ public WorkflowState getSubWorkflow() { return subWorkflow; } @@ -27,6 +35,10 @@ public class WorkflowStepState extends WorkflowStepContextState { this.subWorkflow = subWorkflow; } + /** + * Return the state of each target node if this step runs on each target node + * @return + */ public Map getNodeStates() { return nodeStates; } From 1a1e610b20e25972959f82920622ea1115327500 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 10:37:02 -0800 Subject: [PATCH 23/89] Deprecate incorrect method names --- .../java/org/rundeck/api/RundeckClient.java | 47 +++++++++++++++++-- .../org/rundeck/api/RundeckClientTest.java | 11 +++-- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index aa54b7b..063636d 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -3261,9 +3261,28 @@ public class RundeckClient implements Serializable { * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + * @deprecated renamed for clarity use {@link #getExecutionOutput(Long, int, int, long, int)} */ public RundeckOutput getJobExecutionOutput(Long executionId, int offset, int lastlines, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + return getExecutionOutput(executionId, offset, lastlines, lastmod, maxlines); + } + /** + * Get the execution output of the given job + * + * @param executionId identifier of the execution - mandatory + * @param offset byte offset to read from in the file. 0 indicates the beginning. + * @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. + * @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 + * @param maxlines maximum number of lines to retrieve forward from the specified offset. + * @return {@link RundeckOutput} + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckOutput getExecutionOutput(Long executionId, int offset, int lastlines, long lastmod, int maxlines) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); ApiPathBuilder param = new ApiPathBuilder( "/execution/", executionId.toString(), @@ -3321,7 +3340,7 @@ public class RundeckClient implements Serializable { * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) */ - public RundeckOutput getJobExecutionOutputForNode(Long executionId, String nodeName, int offset, int lastlines, + public RundeckOutput getExecutionOutputForNode(Long executionId, String nodeName, int offset, int lastlines, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); @@ -3362,7 +3381,7 @@ public class RundeckClient implements Serializable { * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) */ - public RundeckOutput getJobExecutionOutputForStep(Long executionId, String stepCtx, int offset, int lastlines, + public RundeckOutput getExecutionOutputForStep(Long executionId, String stepCtx, int offset, int lastlines, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); @@ -3403,7 +3422,8 @@ public class RundeckClient implements Serializable { * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) */ - public RundeckOutput getJobExecutionOutputForNodeAndStep(Long executionId, String nodeName, String stepCtx, int offset, int lastlines, + public RundeckOutput getExecutionOutputForNodeAndStep(Long executionId, String nodeName, String stepCtx, + int offset, int lastlines, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); @@ -3440,9 +3460,27 @@ public class RundeckClient implements Serializable { * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + * @deprecated renamed for clarity use {@link #getExecutionOutput(Long, int, long, int)} */ public RundeckOutput getJobExecutionOutput(Long executionId, int offset, long lastmod, int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + return getExecutionOutput(executionId, offset, lastmod, maxlines); + } + /** + * Get the execution output of the given job + * + * @param executionId identifier of the execution - mandatory + * @param offset byte offset to read from in the file. 0 indicates the beginning. + * @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 + * @param maxlines maximum number of lines to retrieve forward from the specified offset. + * @return {@link RundeckOutput} + * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) + * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) + * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckOutput getExecutionOutput(Long executionId, int offset, long lastmod, int maxlines) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); ApiPathBuilder param = new ApiPathBuilder("/execution/", executionId.toString(), "/output") .param("offset", offset); @@ -3468,7 +3506,8 @@ public class RundeckClient implements Serializable { * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) */ - public RundeckOutput getJobExecutionOutputState(Long executionId, boolean stateOnly, int offset, long lastmod, int maxlines) + public RundeckOutput getExecutionOutputState(Long executionId, boolean stateOnly, int offset, long lastmod, + int maxlines) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { AssertUtil.notNull(executionId, "executionId is mandatory to get the output of a job execution!"); ApiPathBuilder param = new ApiPathBuilder("/execution/", executionId.toString(), "/output/state") diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index afc90d4..cfcea44 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -1066,7 +1066,7 @@ public class RundeckClientTest { @Betamax(tape = "execution_output_fornode", mode = TapeMode.READ_ONLY) public void executionOutputForNode() throws Exception { final RundeckClient client = createClient(TEST_TOKEN_6, 10); - RundeckOutput output = client.getJobExecutionOutputForNode(146L,"node-14.qa.subgroup.mycompany.com",0,-1,0L,-1); + RundeckOutput output = client.getExecutionOutputForNode(146L, "node-14.qa.subgroup.mycompany.com", 0, -1, 0L, -1); Assert.assertEquals(new Long(1602), output.getExecDuration()); Assert.assertEquals(new Long(146), output.getExecutionId()); @@ -1091,7 +1091,7 @@ public class RundeckClientTest { @Betamax(tape = "execution_output_forstep", mode = TapeMode.READ_ONLY) public void executionOutputForStep() throws Exception { final RundeckClient client = createClient(TEST_TOKEN_6, 10); - RundeckOutput output = client.getJobExecutionOutputForStep(146L,"1",0,-1,0L,-1); + RundeckOutput output = client.getExecutionOutputForStep(146L, "1", 0, -1, 0L, -1); Assert.assertEquals(new Long(1602), output.getExecDuration()); Assert.assertEquals(new Long(146), output.getExecutionId()); @@ -1117,7 +1117,8 @@ public class RundeckClientTest { @Betamax(tape = "execution_output_fornodeandstep", mode = TapeMode.READ_ONLY) public void executionOutputForNodeAndStep() throws Exception { final RundeckClient client = createClient(TEST_TOKEN_6, 10); - RundeckOutput output = client.getJobExecutionOutputForNodeAndStep(146L,"node-14.qa.subgroup.mycompany.com","1",0,-1,0L,-1); + RundeckOutput output = client.getExecutionOutputForNodeAndStep(146L, "node-14.qa.subgroup.mycompany.com", + "1", 0, -1, 0L, -1); Assert.assertEquals(new Long(1602), output.getExecDuration()); Assert.assertEquals(new Long(146), output.getExecutionId()); @@ -1143,7 +1144,7 @@ public class RundeckClientTest { @Betamax(tape = "execution_output_state", mode = TapeMode.READ_ONLY) public void executionOutputState() throws Exception { final RundeckClient client = createClient(TEST_TOKEN_6, 10); - RundeckOutput output = client.getJobExecutionOutputState(146L,false,0,0L,-1); + RundeckOutput output = client.getExecutionOutputState(146L, false, 0, 0L, -1); Assert.assertEquals(new Long(1602), output.getExecDuration()); Assert.assertEquals(new Long(146), output.getExecutionId()); @@ -1168,7 +1169,7 @@ public class RundeckClientTest { @Betamax(tape = "execution_output_state_only", mode = TapeMode.READ_ONLY) public void executionOutputStateOnly() throws Exception { final RundeckClient client = createClient(TEST_TOKEN_6, 10); - RundeckOutput output = client.getJobExecutionOutputState(146L,true,0,0L,-1); + RundeckOutput output = client.getExecutionOutputState(146L, true, 0, 0L, -1); Assert.assertEquals(new Long(1602), output.getExecDuration()); Assert.assertEquals(new Long(146), output.getExecutionId()); From b1c15fc07b869b6294b0c08fbf4c931493974282 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 10:51:14 -0800 Subject: [PATCH 24/89] stepNum should be an integer in WorkflowStepContextState --- .../rundeck/api/domain/RundeckExecutionState.java | 2 +- .../rundeck/api/domain/WorkflowStepContextState.java | 6 +++--- .../rundeck/api/parser/WorkflowStepStateParser.java | 2 +- src/site/confluence/status.confluence | 3 ++- .../api/parser/WorkflowStepStateParserTest.java | 12 ++++++------ 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java index 518a1d8..c06a4bf 100644 --- a/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java +++ b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java @@ -13,7 +13,7 @@ public class RundeckExecutionState extends WorkflowState{ private Map> nodeStates; /** - * Return the set of all rundeck nodes targetted in this execution + * Return the set of all rundeck nodes targeted in this execution * @return */ public Set getAllNodes() { diff --git a/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java index e54ab7d..ef6e80c 100644 --- a/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java +++ b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java @@ -5,7 +5,7 @@ package org.rundeck.api.domain; */ public class WorkflowStepContextState extends BaseState { private String stepContextId; - private String stepNum; + private int stepNum; /** * The context id for the step in the form "#[/#[/#[...]]]" where "#" is a number @@ -23,11 +23,11 @@ public class WorkflowStepContextState extends BaseState { * The step number of this step in the current workflow, 1 indexed. * @return */ - public String getStepNum() { + public int getStepNum() { return stepNum; } - public void setStepNum(String stepNum) { + public void setStepNum(int stepNum) { this.stepNum = stepNum; } } diff --git a/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java b/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java index ede8d7c..55b6386 100644 --- a/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java +++ b/src/main/java/org/rundeck/api/parser/WorkflowStepStateParser.java @@ -28,7 +28,7 @@ public class WorkflowStepStateParser implements XmlNodeParser BaseStateParser.parseBaseState(targetNode, state); state.setStepContextId(StringUtils.trimToNull(targetNode.valueOf("@stepctx"))); - state.setStepNum(StringUtils.trimToNull(targetNode.valueOf("@id"))); + state.setStepNum(Integer.valueOf(targetNode.valueOf("@id"))); state.setNodeStep(Boolean.valueOf(StringUtils.trimToNull(targetNode.valueOf("nodeStep")))); if (Boolean.valueOf(StringUtils.trimToNull(targetNode.valueOf("hasSubworkflow")))) { //parse sub workflow diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence index 5e617d2..7b31a17 100644 --- a/src/site/confluence/status.confluence +++ b/src/site/confluence/status.confluence @@ -83,11 +83,12 @@ h2. RunDeck API version 9 * list running executions across all projects - OK * include project name in execution results - OK * Add uuidOption parameter to allow removing imported UUIDs to avoid creation conflicts - OK + h2. RunDeck API version 10 [Documentation of the RunDeck API version 10|http://rundeck.org/2.0.0/api/index.html] -* Execution State - Retrieve workflow step and node state information - *TODO* +* Execution State - Retrieve workflow step and node state information - OK * Execution Output with State - Retrieve log output with state change information - OK * Execution Output - Retrieve log output for a particular node or step - OK * Execution Info - added successfulNodes and failedNodes detail. - OK diff --git a/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java b/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java index 418ecdb..3429369 100644 --- a/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java +++ b/src/test/java/org/rundeck/api/parser/WorkflowStepStateParserTest.java @@ -26,7 +26,7 @@ public class WorkflowStepStateParserTest { Assert.assertEquals(null, stepState.getSubWorkflow()); Assert.assertNotNull(stepState.getNodeStates()); Assert.assertEquals("1", stepState.getStepContextId()); - Assert.assertEquals("1", stepState.getStepNum()); + Assert.assertEquals(1, stepState.getStepNum()); Assert.assertEquals(1390066159000L, stepState.getStartTime().getTime()); Assert.assertEquals(1390066160000L, stepState.getEndTime().getTime()); Assert.assertEquals(1390066160000L, stepState.getUpdateTime().getTime()); @@ -42,7 +42,7 @@ public class WorkflowStepStateParserTest { Assert.assertTrue(expectedTargetNodes.contains(s)); WorkflowStepContextState workflowStepContextState = stepState.getNodeStates().get(s); Assert.assertEquals("1", workflowStepContextState.getStepContextId()); - Assert.assertEquals("1", workflowStepContextState.getStepNum()); + Assert.assertEquals(1, workflowStepContextState.getStepNum()); Assert.assertEquals(1390066159000L + (i * 1000), workflowStepContextState.getStartTime().getTime()); Assert.assertEquals(1390066159000L + (i * 1000), workflowStepContextState.getEndTime().getTime()); Assert.assertEquals(1390066159000L + (i * 1000), workflowStepContextState.getUpdateTime().getTime()); @@ -63,7 +63,7 @@ public class WorkflowStepStateParserTest { Assert.assertEquals(true, stepState.isNodeStep()); Assert.assertEquals(null, stepState.getSubWorkflow()); Assert.assertEquals("1", stepState.getStepContextId()); - Assert.assertEquals("1", stepState.getStepNum()); + Assert.assertEquals(1, stepState.getStepNum()); Assert.assertNotNull(stepState.getNodeStates()); Assert.assertEquals(1390066061000L, stepState.getStartTime().getTime()); Assert.assertEquals(1390066066000L, stepState.getEndTime().getTime()); @@ -75,7 +75,7 @@ public class WorkflowStepStateParserTest { WorkflowStepContextState workflowStepContextState = stepState.getNodeStates().get("dignan"); Assert.assertEquals("1", workflowStepContextState.getStepContextId()); - Assert.assertEquals("1", workflowStepContextState.getStepNum()); + Assert.assertEquals(1, workflowStepContextState.getStepNum()); Assert.assertEquals(1390066061000L, workflowStepContextState.getStartTime().getTime()); Assert.assertEquals(1390066066000L, workflowStepContextState.getEndTime().getTime()); Assert.assertEquals(1390066066000L, workflowStepContextState.getUpdateTime().getTime()); @@ -95,7 +95,7 @@ public class WorkflowStepStateParserTest { Assert.assertNotNull(stepState.getSubWorkflow()); Assert.assertNull(stepState.getNodeStates()); Assert.assertEquals("2", stepState.getStepContextId()); - Assert.assertEquals("2",stepState.getStepNum()); + Assert.assertEquals(2, stepState.getStepNum()); Assert.assertEquals(1390066066000L, stepState.getStartTime().getTime()); Assert.assertNull(stepState.getEndTime()); Assert.assertEquals(1390066066000L, stepState.getUpdateTime().getTime()); @@ -112,7 +112,7 @@ public class WorkflowStepStateParserTest { Assert.assertNull(stepState1.getSubWorkflow()); Assert.assertNotNull(stepState1.getNodeStates()); Assert.assertEquals("2/1", stepState1.getStepContextId()); - Assert.assertEquals("1", stepState1.getStepNum()); + Assert.assertEquals(1, stepState1.getStepNum()); Assert.assertEquals(1390066067000L, stepState1.getStartTime().getTime()); Assert.assertNull(stepState1.getEndTime()); Assert.assertEquals(1390066067000L, stepState1.getUpdateTime().getTime()); From f84ddd6ad8652cc4de496cc94cdb0848862a78c8 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 11:11:52 -0800 Subject: [PATCH 25/89] Remove client methods that were scheduled for removal in v10 --- .../java/org/rundeck/api/RundeckClient.java | 1559 +---------------- src/site/confluence/status.confluence | 1 + .../org/rundeck/api/RundeckClientTest.java | 134 +- 3 files changed, 6 insertions(+), 1688 deletions(-) diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 063636d..9d14e98 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -41,25 +41,6 @@ import java.util.concurrent.TimeUnit; /** * Main entry point to talk to a RunDeck instance. - *

- * Deprecation Warning: These methods which take multiple arguments are deprecated in - * favor of single argument versions, and associated Builder classes to generate them. - * These methods will be removed in version 10 of this library: - *

    - *
  • triggerAdhocScript(...), use {@link #triggerAdhocScript(RunAdhocScript)} and {@link RunAdhocScriptBuilder}
  • - *
  • runAdhocScript(...), use {@link #runAdhocScript(RunAdhocScript)} and {@link RunAdhocScriptBuilder}
  • - *
  • triggerAdhocCommand(...), use {@link #triggerAdhocCommand(RunAdhocCommand)} and {@link - * RunAdhocCommandBuilder}
  • - *
  • runAdhocCommand(...), use {@link #runAdhocCommand(RunAdhocCommand)} and {@link - * RunAdhocCommandBuilder}
  • - *
  • triggerJob(...), use {@link #triggerJob(RunJob)} and {@link - * RunJobBuilder}
  • - *
  • runJob(...), use {@link #runJob(RunJob)} and {@link - * RunJobBuilder}
  • - *
  • importJobs(...), use {@link #importJobs(RundeckJobsImport)} and {@link #importJobs(String, RundeckJobsImport)}
  • - *
- *

- *

* You have 2 methods for authentication : login-based or token-based. If you want to use the first, you need to provide * both a "login" and a "password". Otherwise, just provide a "token" (also called "auth-token"). See the RunDeck * documentation for generating such a token.

@@ -123,10 +104,10 @@ public class RundeckClient implements Serializable { public static final transient String API_ENDPOINT = API + API_VERSION; /** Default value for the "pooling interval" used when running jobs/commands/scripts */ - private static final transient long DEFAULT_POOLING_INTERVAL = 5; + public static final transient long DEFAULT_POOLING_INTERVAL = 5; /** Default unit of the "pooling interval" used when running jobs/commands/scripts */ - private static final transient TimeUnit DEFAULT_POOLING_UNIT = TimeUnit.SECONDS; + public static final TimeUnit DEFAULT_POOLING_UNIT = TimeUnit.SECONDS; /** URL of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc) */ private final String url; @@ -202,7 +183,7 @@ public class RundeckClient implements Serializable { * @throws IllegalArgumentException if the url or token is blank (null, empty or whitespace) * @deprecated Use the builder {@link RundeckClientBuilder}, this method will not be public in version 10 of this library. */ - public RundeckClient(String url, String token, String sessionID, boolean useToken) throws IllegalArgumentException { + private RundeckClient(String url, String token, String sessionID, boolean useToken) throws IllegalArgumentException { this(url); if(useToken){ @@ -260,7 +241,7 @@ public class RundeckClient implements Serializable { } /** - * @deprecated Use {@link #testAuth()} + * @deprecated Use {@link #testAuth()}, will be removed in version 12 of this library. * @see #testAuth() */ @Deprecated @@ -635,193 +616,6 @@ public class RundeckClient implements Serializable { return new ApiCall(this).get(new ApiPathBuilder("/job/", jobId).param("format", format)); } - /** - * Import the definitions of jobs, from the given file - * - * @param filename of the file containing the jobs definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the filename or fileType is blank (null, empty or whitespace), or the - * fileType is invalid - * @throws IOException if we failed to read the file - * @see #importJobs(InputStream, String) - * @see #importJobs(String, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this library. - */ - public RundeckJobsImportResult importJobs(String filename, String fileType) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); - return importJobs(filename, FileType.valueOf(StringUtils.upperCase(fileType))); - } - - /** - * Import the definitions of jobs, from the given file - * - * @param filename of the file containing the jobs definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the filename is blank (null, empty or whitespace), or the fileType is null - * @throws IOException if we failed to read the file - * @see #importJobs(InputStream, FileType) - * @see #importJobs(String, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this - * library. - */ - public RundeckJobsImportResult importJobs(String filename, FileType fileType) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return importJobs(filename, fileType, (RundeckJobsImportMethod) null); - } - - /** - * Import the definitions of jobs, from the given file, using the given behavior - * - * @param filename of the file containing the jobs definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @param importBehavior see {@link RundeckJobsImportMethod} - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the filename or fileType is blank (null, empty or whitespace), or the - * fileType or behavior is not valid - * @throws IOException if we failed to read the file - * @see #importJobs(InputStream, String, String) - * @see #importJobs(String, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this - * library. - */ - public RundeckJobsImportResult importJobs(String filename, String fileType, String importBehavior) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, - IOException { - AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); - return importJobs(filename, - FileType.valueOf(StringUtils.upperCase(fileType)), - RundeckJobsImportMethod.valueOf(StringUtils.upperCase(importBehavior))); - } - - /** - * Import the definitions of jobs, from the given file, using the given behavior - * - * @param filename of the file containing the jobs definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @param importBehavior see {@link RundeckJobsImportMethod} - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the filename is blank (null, empty or whitespace), or the fileType is null - * @throws IOException if we failed to read the file - * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(String, RundeckJobsImport)}, this method will be removed in version 10 of - * this library. - */ - public RundeckJobsImportResult importJobs(String filename, FileType fileType, RundeckJobsImportMethod importBehavior) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, - IOException { - AssertUtil.notBlank(filename, "filename (of jobs file) is mandatory to import jobs !"); - FileInputStream stream = null; - try { - stream = FileUtils.openInputStream(new File(filename)); - return importJobs(stream, fileType, importBehavior); - } finally { - IOUtils.closeQuietly(stream); - } - } - - /** - * Import the definitions of jobs, from the given input stream - * - * @param stream inputStream for reading the definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the stream is null, or the fileType is blank (null, empty or whitespace) or - * invalid - * @see #importJobs(String, String) - * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this - * library. - */ - public RundeckJobsImportResult importJobs(InputStream stream, String fileType) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); - return importJobs(stream, FileType.valueOf(StringUtils.upperCase(fileType))); - } - - /** - * Import the definitions of jobs, from the given input stream - * - * @param stream inputStream for reading the definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the stream or fileType is null - * @see #importJobs(String, FileType) - * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this - * library. - */ - public RundeckJobsImportResult importJobs(InputStream stream, FileType fileType) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return importJobs(stream, fileType, (RundeckJobsImportMethod) null); - } - - /** - * Import the definitions of jobs, from the given input stream, using the given behavior - * - * @param stream inputStream for reading the definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @param importBehavior see {@link RundeckJobsImportMethod} - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the stream is null, or the fileType is blank (null, empty or whitespace), or - * the fileType or behavior is not valid - * @see #importJobs(String, String, String) - * @see #importJobs(InputStream, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this - * library. - */ - public RundeckJobsImportResult importJobs(InputStream stream, String fileType, String importBehavior) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - AssertUtil.notBlank(fileType, "fileType is mandatory to import jobs !"); - return importJobs(stream, - FileType.valueOf(StringUtils.upperCase(fileType)), - RundeckJobsImportMethod.valueOf(StringUtils.upperCase(importBehavior))); - } - - /** - * Import the definitions of jobs, from the given input stream, using the given behavior - * - * @param stream inputStream for reading the definitions - mandatory - * @param fileType type of the file. See {@link FileType} - mandatory - * @param importBehavior see {@link RundeckJobsImportMethod} - * @return a {@link RundeckJobsImportResult} instance - won't be null - * @throws RundeckApiException in case of error when calling the API - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the stream or fileType is null - * @see #importJobs(String, FileType, RundeckJobsImportMethod) - * @deprecated use {@link #importJobs(RundeckJobsImport)}, this method will be removed in version 10 of this - * library. - */ - public RundeckJobsImportResult importJobs(InputStream stream, FileType fileType, - RundeckJobsImportMethod importBehavior) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return importJobs(RundeckJobsImportBuilder.builder().setStream(stream).setFileType(fileType) - .setJobsImportMethod(importBehavior).build()); - } /** * Import the definitions of jobs, from the given input stream, using the given behavior @@ -957,93 +751,6 @@ public class RundeckClient implements Serializable { new BulkDeleteParser("result/deleteJobs")); } - /** - * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the - * end of the job execution) - * - * @param jobId identifier of the job - mandatory - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties, Properties) - * @see #runJob(String) - * @deprecated use {@link #triggerJob(RunJob)}, this method will be removed in version 10 of this library - */ - public RundeckExecution triggerJob(String jobId) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return triggerJob(jobId, null); - } - - /** - * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the - * end of the job execution) - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties, Properties) - * @see #runJob(String, Properties) - * @deprecated use {@link #triggerJob(RunJob)}, this method will be removed in version 10 of this library - */ - public RundeckExecution triggerJob(String jobId, Properties options) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerJob(jobId, options, null); - } - - /** - * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the - * end of the job execution) - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See - * {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String) - * @see #runJob(String, Properties, Properties) - * @deprecated use {@link #triggerJob(RunJob)}, this method will be removed in version 10 of this library - */ - public RundeckExecution triggerJob(String jobId, Properties options, Properties nodeFilters) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerJob(jobId, options, nodeFilters, null); - } - /** - * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the - * end of the job execution) - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See - * {@link NodeFiltersBuilder} - * @param asUser specify a user name to run the job as, must have 'runAs' permission - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String) - * @see #runJob(String, Properties, Properties) - * @deprecated use {@link #triggerJob(RunJob)}, this method will be removed in version 10 of this library - */ - public RundeckExecution triggerJob(String jobId, Properties options, Properties nodeFilters, String asUser) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerJob(RunJobBuilder.builder() - .setJobId(jobId) - .setOptions(options) - .setNodeFilters(nodeFilters) - .setAsUser(asUser) - .build()); - } /** * Trigger the execution of a RunDeck job (identified by the given ID), and return immediately (without waiting the * end of the job execution) @@ -1069,72 +776,6 @@ public class RundeckClient implements Serializable { return new ApiCall(this).get(apiPath, new ExecutionParser("result/executions/execution")); } - /** - * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. - * We will poll the RunDeck server at regular interval (every 5 seconds) to know if the execution is finished (or - * aborted) or is still running. - * - * @param jobId identifier of the job - mandatory - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String) - * @see #runJob(String, Properties, Properties, long, TimeUnit) - * @deprecated use {@link #runJob(RunJob, long, java.util.concurrent.TimeUnit)}, - * this method will be removed in version 10 of this library - */ - public RundeckExecution runJob(String jobId) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return runJob(jobId, null); - } - - /** - * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. - * We will poll the RunDeck server at regular interval (every 5 seconds) to know if the execution is finished (or - * aborted) or is still running. - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties) - * @see #runJob(String, Properties, Properties, long, TimeUnit) - * @deprecated use {@link #runJob(RunJob, long, java.util.concurrent.TimeUnit)}, - * this method will be removed in version 10 of this library - */ - public RundeckExecution runJob(String jobId, Properties options) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runJob(jobId, options, null); - } - - /** - * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. - * We will poll the RunDeck server at regular interval (every 5 seconds) to know if the execution is finished (or - * aborted) or is still running. - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See - * {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties, Properties) - * @see #runJob(String, Properties, Properties, long, TimeUnit) - * @deprecated use {@link #runJob(RunJob, long, java.util.concurrent.TimeUnit)}, - * this method will be removed in version 10 of this library - */ - public RundeckExecution runJob(String jobId, Properties options, Properties nodeFilters) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runJob(jobId, options, nodeFilters, DEFAULT_POOLING_INTERVAL, DEFAULT_POOLING_UNIT); - } /** * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. @@ -1156,87 +797,6 @@ public class RundeckClient implements Serializable { RundeckApiTokenException, IllegalArgumentException { return runJob(runJob, DEFAULT_POOLING_INTERVAL, DEFAULT_POOLING_UNIT); } - /** - * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. - * We will poll the RunDeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to - * know if the execution is finished (or aborted) or is still running. - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties) - * @see #runJob(String, Properties, Properties, long, TimeUnit) - * @deprecated use {@link #runJob(RunJob, long, java.util.concurrent.TimeUnit)}, - * this method will be removed in version 10 of this library - */ - public RundeckExecution runJob(String jobId, Properties options, long poolingInterval, TimeUnit poolingUnit) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runJob(jobId, options, null, poolingInterval, poolingUnit); - } - - /** - * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. - * We will poll the RunDeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to - * know if the execution is finished (or aborted) or is still running. - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See - * {@link NodeFiltersBuilder} - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties) - * @see #runJob(String, Properties, Properties, long, TimeUnit) - * @deprecated use {@link #runJob(RunJob, long, java.util.concurrent.TimeUnit)}, - * this method will be removed in version 10 of this library - */ - public RundeckExecution runJob(String jobId, Properties options, Properties nodeFilters, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException { - return runJob(jobId, options, nodeFilters, null, poolingInterval, poolingUnit); - } - /** - * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. - * We will poll the RunDeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to - * know if the execution is finished (or aborted) or is still running. - * - * @param jobId identifier of the job - mandatory - * @param options of the job - optional. See {@link OptionsBuilder}. - * @param nodeFilters for overriding the nodes on which the job will be executed - optional. See - * {@link NodeFiltersBuilder} - * @param asUser specify a user name to run the job as, must have 'runAs' permission - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent job with this ID) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) - * @see #triggerJob(String, Properties) - * @see #runJob(String, Properties, Properties, long, TimeUnit) - * @deprecated use {@link #runJob(RunJob, long, java.util.concurrent.TimeUnit)}, this method will be removed in version 10 of this library - */ - public RundeckExecution runJob(String jobId, Properties options, Properties nodeFilters, String asUser, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException { - return runJob(RunJobBuilder.builder() - .setJobId(jobId) - .setOptions(options) - .setNodeFilters(nodeFilters) - .setAsUser(asUser) - .build(), poolingInterval, poolingUnit); - } /** * Run a RunDeck job (identified by the given ID), and wait until its execution is finished (or aborted) to return. @@ -1283,101 +843,7 @@ public class RundeckClient implements Serializable { * Ad-hoc commands */ - /** - * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution). - * The command will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #triggerAdhocCommand(String, String, Properties, Integer, Boolean) - * @see #runAdhocCommand(String, String) - * @deprecated use {@link #triggerAdhocCommand(RunAdhocCommand)}, will be removed in version 10 of this library - */ - public RundeckExecution triggerAdhocCommand(String project, String command) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocCommand(project, command, null); - } - /** - * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution). - * The command will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #triggerAdhocCommand(String, String, Properties, Integer, Boolean) - * @see #runAdhocCommand(String, String, Properties) - * @deprecated use {@link #triggerAdhocCommand(RunAdhocCommand)}, will be removed in version 10 of this library - */ - public RundeckExecution triggerAdhocCommand(String project, String command, Properties nodeFilters) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocCommand(project, command, nodeFilters, null, null); - } - - /** - * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution). - * The command will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #triggerAdhocCommand(String, String) - * @see #runAdhocCommand(String, String, Properties) - * @deprecated use {@link #triggerAdhocCommand(RunAdhocCommand)}, will be removed in version 10 of this library - */ - public RundeckExecution triggerAdhocCommand(String project, String command, Properties nodeFilters, - Integer nodeThreadcount, Boolean nodeKeepgoing) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocCommand(project, command, nodeFilters, nodeThreadcount, nodeKeepgoing, null); - } - /** - * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution). - * The command will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @param asUser specify a user name to run the job as, must have 'runAs' permission - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #triggerAdhocCommand(String, String) - * @see #runAdhocCommand(String, String, Properties) - * @deprecated use {@link #triggerAdhocCommand(RunAdhocCommand)}, will be removed in version 10 of this library - */ - public RundeckExecution triggerAdhocCommand(String project, String command, Properties nodeFilters, - Integer nodeThreadcount, Boolean nodeKeepgoing, String asUser) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocCommand(RunAdhocCommandBuilder.builder() - .setProject(project) - .setCommand(command) - .setNodeFilters(nodeFilters) - .setNodeThreadcount(nodeThreadcount) - .setNodeKeepgoing(nodeKeepgoing) - .setAsUser(asUser) - .build()); - } /** * Trigger the execution of an ad-hoc command, and return immediately (without waiting the end of the execution). * The command will be dispatched to nodes, accordingly to the nodeFilters parameter. @@ -1410,134 +876,6 @@ public class RundeckClient implements Serializable { return getExecution(execution.getId()); } - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The command will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #runAdhocCommand(String, String, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocCommand(String, String) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(project, command, null); - } - - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The command will not be dispatched to nodes, but be executed on the - * RunDeck server. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #runAdhocCommand(String, String, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocCommand(String, String) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand, long, java.util.concurrent.TimeUnit)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command, long poolingInterval, TimeUnit poolingUnit) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(project, command, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The command will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #runAdhocCommand(String, String, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocCommand(String, String, Properties) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(project, command, nodeFilters, null, null); - } - - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The command will be dispatched to nodes, accordingly to the - * nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #runAdhocCommand(String, String, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocCommand(String, String, Properties) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand, long, java.util.concurrent.TimeUnit)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters, - long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(project, command, nodeFilters, null, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The command will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #runAdhocCommand(String, String, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocCommand(String, String, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters, - Integer nodeThreadcount, Boolean nodeKeepgoing) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(project, - command, - nodeFilters, - nodeThreadcount, - nodeKeepgoing, - DEFAULT_POOLING_INTERVAL, - DEFAULT_POOLING_UNIT); - } /** * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck @@ -1560,70 +898,6 @@ public class RundeckClient implements Serializable { DEFAULT_POOLING_UNIT); } - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The command will be dispatched to nodes, accordingly to the - * nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #triggerAdhocCommand(String, String, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand, long, java.util.concurrent.TimeUnit)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters, - Integer nodeThreadcount, Boolean nodeKeepgoing, long poolingInterval, TimeUnit poolingUnit) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(project, command, nodeFilters, nodeThreadcount, nodeKeepgoing, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The command will be dispatched to nodes, accordingly to the - * nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param command to be executed - mandatory - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or command is blank (null, empty or whitespace) - * @see #triggerAdhocCommand(String, String, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocCommand(RunAdhocCommand, long, java.util.concurrent.TimeUnit)}, this method will - * be removed in version 10 of this library - */ - public RundeckExecution runAdhocCommand(String project, String command, Properties nodeFilters, - Integer nodeThreadcount, Boolean nodeKeepgoing, String asUser, long poolingInterval, TimeUnit poolingUnit) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return runAdhocCommand(RunAdhocCommandBuilder.builder() - .setProject(project) - .setCommand(command) - .setNodeFilters(nodeFilters) - .setNodeThreadcount(nodeThreadcount) - .setNodeKeepgoing(nodeKeepgoing) - .setAsUser(asUser) - .build(), poolingInterval, poolingUnit); - } /** * Run an ad-hoc command, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is @@ -1666,323 +940,6 @@ public class RundeckClient implements Serializable { * Ad-hoc scripts */ - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, String) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, String scriptFilename) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return triggerAdhocScript(project, scriptFilename, null); - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, String, Properties) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, String scriptFilename, Properties options) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, - IOException { - return triggerAdhocScript(project, scriptFilename, options, null); - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, String, Properties, Properties) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException, IOException { - return triggerAdhocScript(project, scriptFilename, options, nodeFilters, null, null); - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return triggerAdhocScript(project, scriptFilename, options, nodeFilters, nodeThreadcount, nodeKeepgoing, null); - } - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing,String asUser) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return triggerAdhocScript(project, scriptFilename, ParametersUtil.generateArgString(options), nodeFilters, - nodeThreadcount, nodeKeepgoing, asUser); - } - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param argString arguments of the script - optional. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, String scriptFilename, String argString, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing,String asUser) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - AssertUtil.notBlank(scriptFilename, "scriptFilename is mandatory to trigger an ad-hoc script !"); - FileInputStream stream = null; - try { - stream = FileUtils.openInputStream(new File(scriptFilename)); - return triggerAdhocScript(RunAdhocScriptBuilder.builder() - .setProject(project) - .setScript(stream) - .setNodeFilters(nodeFilters) - .setArgString(argString) - .setNodeThreadcount(nodeThreadcount) - .setNodeKeepgoing(nodeKeepgoing) - .setAsUser(asUser) - .build()); - } finally { - IOUtils.closeQuietly(stream); - } - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, InputStream) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, InputStream script) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocScript(project, script, null); - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, InputStream, Properties) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, InputStream script, Properties options) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocScript(project, script, options, null); - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, InputStream, Properties, Properties) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException { - return triggerAdhocScript(project, script, options, nodeFilters, null, null); - } - - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocScript(project, script, options, nodeFilters, nodeThreadcount, nodeKeepgoing, null); - } - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing, String asUser) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocScript(project, script, ParametersUtil.generateArgString(options), nodeFilters, - nodeThreadcount, nodeKeepgoing, asUser); - } - /** - * Trigger the execution of an ad-hoc script, and return immediately (without waiting the end of the execution). The - * script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param argString arguments of the script - optional. - * @param nodeFilters for selecting nodes on which the command will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the newly created (and running) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @deprecated use {@link #triggerAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution triggerAdhocScript(String project, InputStream script, String argString, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing, String asUser) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return triggerAdhocScript(RunAdhocScriptBuilder.builder() - .setProject(project) - .setScript(script) - .setNodeFilters(nodeFilters) - .setArgString(argString) - .setNodeThreadcount(nodeThreadcount) - .setNodeKeepgoing(nodeKeepgoing) - .setAsUser(asUser) - .build()); - } /** * Trigger the execution of an ad-hoc script read from a file, and return immediately (without waiting the end of @@ -2053,440 +1010,6 @@ public class RundeckClient implements Serializable { return getExecution(execution.getId()); } - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, scriptFilename, null); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will not be dispatched to nodes, but be executed on the - * RunDeck server. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException, IOException { - return runAdhocScript(project, scriptFilename, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, Properties options) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, - IOException { - return runAdhocScript(project, scriptFilename, options, null); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will not be dispatched to nodes, but be executed on the - * RunDeck server. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, Properties options, - long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, scriptFilename, options, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String, Properties, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException, IOException { - return runAdhocScript(project, scriptFilename, options, nodeFilters, null, null); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to the nodeFilters - * parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String, Properties, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters, long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, scriptFilename, options, nodeFilters, null, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, - scriptFilename, - options, - nodeFilters, - nodeThreadcount, - nodeKeepgoing, - DEFAULT_POOLING_INTERVAL, - DEFAULT_POOLING_UNIT); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to the nodeFilters - * parameter. - * - * @param project name of the project - mandatory - * @param scriptFilename filename of the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project or scriptFilename is blank (null, empty or whitespace) - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, String, Properties, Properties, Integer, Boolean) - * - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, String scriptFilename, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException, IOException { - AssertUtil.notBlank(scriptFilename, "scriptFilename is mandatory to run an ad-hoc script !"); - FileInputStream stream = null; - try { - stream = FileUtils.openInputStream(new File(scriptFilename)); - return runAdhocScript(RunAdhocScriptBuilder.builder() - .setProject(project) - .setScript(stream) - .setNodeFilters(nodeFilters) - .setArgString(ParametersUtil.generateArgString(options)) - .setNodeThreadcount(nodeThreadcount) - .setNodeKeepgoing(nodeKeepgoing) - .build(), - poolingInterval, - poolingUnit); - } finally { - IOUtils.closeQuietly(stream); - } - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, script, null); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will not be dispatched to nodes, but be executed on the - * RunDeck server. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException, IOException { - return runAdhocScript(project, script, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will not be dispatched to nodes, but be executed on the RunDeck server. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options) - throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, - IOException { - return runAdhocScript(project, script, options, null); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will not be dispatched to nodes, but be executed on the - * RunDeck server. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options, - long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, - RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, script, options, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException, IOException { - return runAdhocScript(project, script, options, nodeFilters, null, null); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to the nodeFilters - * parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties, Properties) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters, long poolingInterval, TimeUnit poolingUnit) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, script, options, nodeFilters, null, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (every 5 seconds) to know if the execution is finished (or aborted) or is still - * running. The script will be dispatched to nodes, accordingly to the nodeFilters parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocScript(RunAdhocScript)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing) throws RundeckApiException, - RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException, IOException { - return runAdhocScript(project, - script, - options, - nodeFilters, - nodeThreadcount, - nodeKeepgoing, - DEFAULT_POOLING_INTERVAL, - DEFAULT_POOLING_UNIT); - } /** * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck @@ -2510,80 +1033,6 @@ public class RundeckClient implements Serializable { DEFAULT_POOLING_UNIT); } - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to the nodeFilters - * parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException { - return runAdhocScript(project, script, options, nodeFilters, nodeThreadcount, nodeKeepgoing, null, poolingInterval, poolingUnit); - } - - /** - * Run an ad-hoc script, and wait until its execution is finished (or aborted) to return. We will poll the RunDeck - * server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the execution is - * finished (or aborted) or is still running. The script will be dispatched to nodes, accordingly to the nodeFilters - * parameter. - * - * @param project name of the project - mandatory - * @param script inputStream for reading the script to be executed - mandatory - * @param options of the script - optional. See {@link OptionsBuilder}. - * @param nodeFilters for selecting nodes on which the script will be executed. See {@link NodeFiltersBuilder} - * @param nodeThreadcount thread count to use (for parallelizing when running on multiple nodes) - optional - * @param nodeKeepgoing if true, continue executing on other nodes even if some fail - optional - * @param poolingInterval for checking the status of the execution. Must be > 0. - * @param poolingUnit unit (seconds, milli-seconds, ...) of the interval. Default to seconds. - * - * @return a {@link RundeckExecution} instance for the (finished/aborted) execution - won't be null - * - * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) - * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) - * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) - * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) or the script is null - * @throws IOException if we failed to read the file - * @see #runAdhocScript(String, String, Properties, Properties, Integer, Boolean, long, TimeUnit) - * @see #triggerAdhocScript(String, InputStream, Properties, Properties, Integer, Boolean) - * @deprecated use {@link #runAdhocScript(RunAdhocScript, long, java.util.concurrent.TimeUnit)}, this method will be - * removed in version 10 of this library - */ - public RundeckExecution runAdhocScript(String project, InputStream script, Properties options, - Properties nodeFilters, Integer nodeThreadcount, Boolean nodeKeepgoing, String asUser, long poolingInterval, - TimeUnit poolingUnit) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, - IllegalArgumentException { - return runAdhocScript(RunAdhocScriptBuilder.builder() - .setProject(project) - .setScript(script) - .setNodeFilters(nodeFilters) - .setArgString(ParametersUtil.generateArgString(options)) - .setNodeThreadcount(nodeThreadcount) - .setNodeKeepgoing(nodeKeepgoing) - .setAsUser(asUser) - .build(), poolingInterval, poolingUnit); - } - /** * Run an ad-hoc script read from a file, and wait until its execution is finished (or aborted) to return. We will * poll the RunDeck server at regular interval (configured by the poolingInterval/poolingUnit couple) to know if the diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence index 7b31a17..a665d74 100644 --- a/src/site/confluence/status.confluence +++ b/src/site/confluence/status.confluence @@ -92,3 +92,4 @@ h2. RunDeck API version 10 * Execution Output with State - Retrieve log output with state change information - OK * Execution Output - Retrieve log output for a particular node or step - OK * Execution Info - added successfulNodes and failedNodes detail. - OK +* Deprecation: Remove methods deprecated until version 10. - *TODO* diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index cfcea44..6e755db 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -364,22 +364,7 @@ public class RundeckClientTest { Assert.assertNull(delete.getMessage()); Assert.assertEquals("3a6d16be-4268-4d26-86a9-cebc1781f768", delete.getId()); } - @Test - @Betamax(tape = "trigger_job_basic") - public void triggerJobDeprecatedBasic() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - final RundeckExecution test - = client.triggerJob("3170ba0e-6093-4b58-94d2-52988aefbfc9", null, null, null); - - Assert.assertEquals((Long) 19L, test.getId()); - Assert.assertEquals(null, test.getArgstring()); - Assert.assertEquals(null, test.getAbortedBy()); - Assert.assertEquals("echo hi there ${job.username} ; sleep 90", test.getDescription()); - Assert.assertEquals("admin", test.getStartedBy()); - Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); - - } @Test @Betamax(tape = "trigger_job_basic") public void triggerJobBasic() throws Exception { @@ -396,22 +381,7 @@ public class RundeckClientTest { Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); } - @Test - @Betamax(tape = "trigger_job_as_user") - public void triggerJobDeprecatedAsUser() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - final RundeckExecution test - = client.triggerJob("3170ba0e-6093-4b58-94d2-52988aefbfc9", null, null, "api-java-client-user-test1"); - - Assert.assertEquals((Long) 20L, test.getId()); - Assert.assertEquals(null, test.getArgstring()); - Assert.assertEquals(null, test.getAbortedBy()); - Assert.assertEquals("echo hi there ${job.username} ; sleep 90", test.getDescription()); - Assert.assertEquals("api-java-client-user-test1", test.getStartedBy()); - Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); - - } @Test @Betamax(tape = "trigger_job_as_user") public void triggerJobAsUser() throws Exception { @@ -431,19 +401,7 @@ public class RundeckClientTest { Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); } - @Test - @Betamax(tape = "trigger_job_as_user_unauthorized") - public void triggerJobDeprecatedAsUserUnauthorized() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - final RundeckExecution test; - try { - test = client.triggerJob("3170ba0e-6093-4b58-94d2-52988aefbfc9",null,null,"api-java-client-user-test2"); - Assert.fail("should not succeed"); - } catch (RundeckApiException e) { - Assert.assertEquals("Not authorized for action \"Run as User\" for Job ID 3170ba0e-6093-4b58-94d2-52988aefbfc9", e.getMessage()); - } - } @Test @Betamax(tape = "trigger_job_as_user_unauthorized") public void triggerJobAsUserUnauthorized() throws Exception { @@ -461,21 +419,7 @@ public class RundeckClientTest { } } - @Test - @Betamax(tape = "trigger_adhoc_command") - public void triggerAdhocCommandDeprecated() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - final RundeckExecution test - = client.triggerAdhocCommand("test", "echo test trigger_adhoc_command"); - - Assert.assertEquals((Long) 23L, test.getId()); - Assert.assertEquals(null, test.getArgstring()); - Assert.assertEquals(null, test.getAbortedBy()); - Assert.assertEquals("echo test trigger_adhoc_command", test.getDescription()); - Assert.assertEquals("admin", test.getStartedBy()); - Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, test.getStatus()); - } @Test @Betamax(tape = "trigger_adhoc_command") @@ -496,21 +440,7 @@ public class RundeckClientTest { Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, test.getStatus()); } - @Test - @Betamax(tape = "trigger_adhoc_command_as_user") - public void triggerAdhocCommandDeprecatedAsUser() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - final RundeckExecution test - = client.triggerAdhocCommand("test", "echo test trigger_adhoc_command_as_user",null,null,null,"api-java-client-test-run-command-as-user1"); - - Assert.assertEquals((Long) 24L, test.getId()); - Assert.assertEquals(null, test.getArgstring()); - Assert.assertEquals(null, test.getAbortedBy()); - Assert.assertEquals("echo test trigger_adhoc_command_as_user", test.getDescription()); - Assert.assertEquals("api-java-client-test-run-command-as-user1", test.getStartedBy()); - Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, test.getStatus()); - } @Test @Betamax(tape = "trigger_adhoc_command_as_user") public void triggerAdhocCommandAsUser() throws Exception { @@ -532,19 +462,7 @@ public class RundeckClientTest { Assert.assertEquals("api-java-client-test-run-command-as-user1", test.getStartedBy()); Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, test.getStatus()); } - @Test - @Betamax(tape = "trigger_adhoc_command_as_user_unauthorized") - public void triggerAdhocCommandDeprecatedAsUserUnauthorized() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - final RundeckExecution test; - try { - test = client.triggerAdhocCommand("test", "echo test trigger_adhoc_command_as_user",null,null,null,"api-java-client-test-run-command-as-user1"); - Assert.fail("should not succeed"); - } catch (RundeckApiException e) { - Assert.assertEquals("Not authorized for action \"Run as User\" for Run Adhoc", e.getMessage()); - } - } @Test @Betamax(tape = "trigger_adhoc_command_as_user_unauthorized") public void triggerAdhocCommandAsUserUnauthorized() throws Exception { @@ -565,24 +483,7 @@ public class RundeckClientTest { } } - @Test - @Betamax(tape = "trigger_adhoc_script") - public void triggerAdhocScriptDeprecated() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - String script = "#!/bin/bash\n" + - "echo test trigger_adhoc_script\n"; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(script.getBytes()); - final RundeckExecution test - = client.triggerAdhocScript("test", byteArrayInputStream,(Properties) null, null, null, null, null); - - Assert.assertEquals((Long) 25L, test.getId()); - Assert.assertEquals(null, test.getArgstring()); - Assert.assertEquals(null, test.getAbortedBy()); - Assert.assertEquals("#!/bin/bash\necho test trigger_adhoc_script", test.getDescription()); - Assert.assertEquals("admin", test.getStartedBy()); - Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); - } @Test @Betamax(tape = "trigger_adhoc_script") public void triggerAdhocScript() throws Exception { @@ -602,24 +503,7 @@ public class RundeckClientTest { Assert.assertEquals("admin", test.getStartedBy()); Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); } - @Test - @Betamax(tape = "trigger_adhoc_script_as_user") - public void triggerAdhocScriptDeprecatedAsUser() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - String script = "#!/bin/bash\n" + - "echo test trigger_adhoc_script\n"; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(script.getBytes()); - final RundeckExecution test - = client.triggerAdhocScript("test", byteArrayInputStream, (Properties) null, null, null, null, "api-java-client-test-adhoc-script-as-user1"); - - Assert.assertEquals((Long) 26L, test.getId()); - Assert.assertEquals(null, test.getArgstring()); - Assert.assertEquals(null, test.getAbortedBy()); - Assert.assertEquals("#!/bin/bash\necho test trigger_adhoc_script", test.getDescription()); - Assert.assertEquals("api-java-client-test-adhoc-script-as-user1", test.getStartedBy()); - Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); - } @Test @Betamax(tape = "trigger_adhoc_script_as_user") public void triggerAdhocScriptAsUser() throws Exception { @@ -639,23 +523,7 @@ public class RundeckClientTest { Assert.assertEquals("api-java-client-test-adhoc-script-as-user1", test.getStartedBy()); Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus()); } - @Test - @Betamax(tape = "trigger_adhoc_script_as_user_unauthorized") - public void triggerAdhocScriptDeprecatedAsUserUnauthorized() throws Exception { - RundeckClient client = createClient(TEST_TOKEN_3, 5); - String script = "#!/bin/bash\n" + - "echo test trigger_adhoc_script\n"; - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(script.getBytes()); - - try{ - final RundeckExecution test - = client.triggerAdhocScript("test", byteArrayInputStream, (Properties) null, null, null, null, "api-java-client-test-adhoc-script-as-user1"); - Assert.fail("should not succeed"); - } catch (RundeckApiException e) { - Assert.assertEquals("Not authorized for action \"Run as User\" for Run Adhoc", e.getMessage()); - } - - } + @Test @Betamax(tape = "trigger_adhoc_script_as_user_unauthorized") public void triggerAdhocScriptAsUserUnauthorized() throws Exception { From 7409d30d933809d502e4df9f0409257f7ada9713 Mon Sep 17 00:00:00 2001 From: Greg Schueler Date: Sat, 18 Jan 2014 11:23:04 -0800 Subject: [PATCH 26/89] Update javadocs and deprecate public constructors for RundeckClient --- src/main/java/org/rundeck/api/ApiCall.java | 3 +- .../java/org/rundeck/api/RundeckClient.java | 43 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index eec4701..daac945 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -115,7 +115,8 @@ class ApiCall { /** * Test the authentication on the RunDeck instance. Will delegate to either {@link #testLoginAuth()} (in case of * login-based auth) or {@link #testTokenAuth()} (in case of token-based auth). - * + * + * @return the login session ID if using login-based auth, otherwise null * @throws RundeckApiLoginException if the login fails (in case of login-based authentication) * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication) * @see #testLoginAuth() diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 9d14e98..61911bc 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -40,18 +40,30 @@ import java.util.Properties; import java.util.concurrent.TimeUnit; /** - * Main entry point to talk to a RunDeck instance. - * You have 2 methods for authentication : login-based or token-based. If you want to use the first, you need to provide - * both a "login" and a "password". Otherwise, just provide a "token" (also called "auth-token"). See the RunDeck + * Rundeck API client. + *

+ * There are three methods for authentication : login-based or token-based or session-based. + * Login authentication requires + * both a "login" and a "password". Token-based requires a "token" (also called "auth-token"). See the RunDeck * documentation for generating such a token.

+ *

+ * Session-based authentication allows re-use of a previous login session. See {@link #testAuth()}. + *

+ *

+ * Deprecation notice: All public constructors for this class are deprecated. Use the {@link RundeckClientBuilder} or {@link #builder()} convenience method to create a RundeckClient. The public constructors will be made non-public in version 12 of this library. + *

*
* Usage :
* *
  * // using login-based authentication :
- * RundeckClient rundeck = new RundeckClient("http://localhost:4440", "admin", "admin");
+ * RundeckClient rundeck = RundeckClient.builder()
+ *                           .url("http://localhost:4440")
+ *                           .login("admin", "admin").build();
  * // or for a token-based authentication :
- * RundeckClient rundeck = new RundeckClient("http://localhost:4440", "PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD");
+ * RundeckClient rundeck = RundeckClient.builder()
+ *                           .url("http://localhost:4440")
+ *                           .token("PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD").build();
  *
  * List<RundeckProject> projects = rundeck.getProjects();
  *
@@ -162,6 +174,9 @@ public class RundeckClient implements Serializable {
      * @param login to use for authentication on the RunDeck instance
      * @param password to use for authentication on the RunDeck instance
      * @throws IllegalArgumentException if the url, login or password is blank (null, empty or whitespace)
+     *
+     * @deprecated Use the builder {@link RundeckClientBuilder} or {@link #builder()}, this method will not be public in version 12 of this
+     * library.
      */
     public RundeckClient(String url, String login, String password) throws IllegalArgumentException {
         this(url);
@@ -181,7 +196,7 @@ public class RundeckClient implements Serializable {
      * @param sessionID to use for session authentication on the RunDeck instance
      * @param useToken should be true if using token, false if using sessionID
      * @throws IllegalArgumentException if the url or token is blank (null, empty or whitespace)
-     * @deprecated Use the builder {@link RundeckClientBuilder}, this method will not be public in version 10 of this library.
+     * @deprecated Use the builder {@link RundeckClientBuilder} or {@link #builder()}, this method will not be public in version 10 of this library.
      */
     private RundeckClient(String url, String token, String sessionID, boolean useToken) throws IllegalArgumentException {
         this(url);
@@ -201,6 +216,16 @@ public class RundeckClient implements Serializable {
     }
 
 
+    /**
+     * Instantiate a new {@link RundeckClient} for the RunDeck instance at the given url,
+     * using token-based authentication. Either token must be valid
+     * @param url of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc)
+     * @param token to use for authentication on the RunDeck instance
+     * @throws IllegalArgumentException if the url or token is blank (null, empty or whitespace)
+     * @deprecated Use the builder {@link RundeckClientBuilder} or {@link #builder()},
+     * this method will not be public in version 12 of this
+     * library.
+     */
     public RundeckClient(String url, String token) throws IllegalArgumentException {
         this(url, token, null, true);
     }
@@ -1710,7 +1735,8 @@ public class RundeckClient implements Serializable {
      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
-     * @deprecated renamed for clarity use {@link #getExecutionOutput(Long, int, int, long, int)}
+     * @deprecated renamed for clarity use {@link #getExecutionOutput(Long, int, int, long, int)}, will be removed in
+     * version 12 of this library
      */
     public RundeckOutput getJobExecutionOutput(Long executionId, int offset, int lastlines, long lastmod, int maxlines)
             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {
@@ -1909,7 +1935,8 @@ public class RundeckClient implements Serializable {
      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
      * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace)
-     * @deprecated renamed for clarity use {@link #getExecutionOutput(Long, int, long, int)}
+     * @deprecated renamed for clarity use {@link #getExecutionOutput(Long, int, long, int)}, will be removed in
+     * version 12 of this library
      */
     public RundeckOutput getJobExecutionOutput(Long executionId, int offset, long lastmod, int maxlines)
             throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException {

From bf13167363477e059f2e5c52f6426db711008e6b Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Sat, 18 Jan 2014 11:28:06 -0800
Subject: [PATCH 27/89] Update project status

---
 src/changes/changes.xml               | 17 +++++++++++++++++
 src/site/confluence/status.confluence |  2 +-
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7a679ae..9263e66 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -22,6 +22,23 @@
     Vincent Behar
   
   
+      
+          
+              Execution State - Retrieve workflow step and node state information
+          
+          
+              Execution Output with State - Retrieve log output with state change information
+          
+          
+              Execution Output - Retrieve log output for a particular node or step.
+          
+          
+              Execution Info - added successfulNodes and failedNodes detail.
+          
+          
+              Remove deprecated RundeckClient methods.
+          
+      
       
           
               Issue #7: Fix authentication to Tomcat container (thanks @katanafleet)
diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index a665d74..3a3d96b 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -92,4 +92,4 @@ h2. RunDeck API version 10
 * Execution Output with State - Retrieve log output with state change information - OK
 * Execution Output - Retrieve log output for a particular node or step - OK
 * Execution Info - added successfulNodes and failedNodes detail. - OK
-* Deprecation: Remove methods deprecated until version 10. - *TODO*
+* Deprecation: Remove methods deprecated until version 10. - OK

From ec439560b5067244aebb77698d470634133d12b7 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Sat, 18 Jan 2014 11:28:59 -0800
Subject: [PATCH 28/89] Update version to 10.0-SNAPSHOT

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 501ebe6..abb0455 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  9.4-SNAPSHOT
+  10.0-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From 8a495f8723abd9da3418c55970e5401efebcfd00 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 09:50:06 -0800
Subject: [PATCH 29/89] [maven-release-plugin] prepare release
 rundeck-api-java-client-10.0

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index abb0455..585ffcd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  10.0-SNAPSHOT
+  10.0
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From e95892e88eb98cf0c31acb0f66edd471955f2b81 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 09:50:11 -0800
Subject: [PATCH 30/89] [maven-release-plugin] prepare for next development
 iteration

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 585ffcd..d284be1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  10.0
+  10.1-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From 7aa512f1ac269f7f9cf7a618549b370f810d1c7c Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 17 Apr 2014 17:40:24 -0700
Subject: [PATCH 31/89] add .travis.yml

---
 .travis.yml | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 .travis.yml

diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..dff5f3a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1 @@
+language: java

From 9ebd63e9536b1baa13a45a92d6d4991dc888f8cd Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Wed, 23 Apr 2014 16:22:02 -0700
Subject: [PATCH 32/89] Add 10.0 release date in changes

---
 src/changes/changes.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 9263e66..ba52cb2 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -22,7 +22,7 @@
     Vincent Behar
   
   
-      
+      
           
               Execution State - Retrieve workflow step and node state information
           

From c3b1cbc39f510c917b1a63ff486e7adcb809db08 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 12:29:37 -0800
Subject: [PATCH 33/89] PRoduces xml stream from a document

---
 .../api/util/DocumentContentProducer.java     | 38 +++++++++++++++++++
 1 file changed, 38 insertions(+)
 create mode 100644 src/main/java/org/rundeck/api/util/DocumentContentProducer.java

diff --git a/src/main/java/org/rundeck/api/util/DocumentContentProducer.java b/src/main/java/org/rundeck/api/util/DocumentContentProducer.java
new file mode 100644
index 0000000..ace7840
--- /dev/null
+++ b/src/main/java/org/rundeck/api/util/DocumentContentProducer.java
@@ -0,0 +1,38 @@
+package org.rundeck.api.util;
+
+import org.apache.http.entity.ContentProducer;
+import org.dom4j.Document;
+import org.dom4j.io.OutputFormat;
+import org.dom4j.io.XMLWriter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * DocumentContentProducer writes XML document to a stream
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class DocumentContentProducer implements ContentProducer {
+    Document document;
+    private OutputFormat format;
+
+    public DocumentContentProducer(final Document document, final OutputFormat format) {
+        this.document = document;
+        this.format = format;
+    }
+
+    public DocumentContentProducer(final Document document) {
+        this.document = document;
+        format = new OutputFormat("", false, "UTF-8");
+    }
+
+    @Override
+    public void writeTo(final OutputStream outstream) throws IOException {
+
+        final XMLWriter xmlWriter = new XMLWriter(outstream, format);
+        xmlWriter.write(document);
+        xmlWriter.flush();
+    }
+}

From bd7e8f5cec2eb2f154818b6218d7d376e3c349d1 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 12:30:07 -0800
Subject: [PATCH 34/89] Allow adding an xml document to the api request

---
 .../java/org/rundeck/api/ApiPathBuilder.java  | 20 ++++++++++++++++++-
 1 file changed, 19 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java
index 22643c8..e749004 100644
--- a/src/main/java/org/rundeck/api/ApiPathBuilder.java
+++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java
@@ -26,6 +26,7 @@ import java.util.Properties;
 import org.apache.commons.lang.StringUtils;
 import org.apache.http.NameValuePair;
 import org.apache.http.message.BasicNameValuePair;
+import org.dom4j.Document;
 import org.rundeck.api.util.ParametersUtil;
 
 /**
@@ -43,6 +44,7 @@ class ApiPathBuilder {
     /** When POSTing, we can add attachments */
     private final Map attachments;
     private final List form = new ArrayList();
+    private Document xmlDocument;
 
     /** Marker for using the right separator between parameters ("?" or "&") */
     private boolean firstParamDone = false;
@@ -266,6 +268,18 @@ class ApiPathBuilder {
         }
         return this;
     }
+    /**
+     * When POSTing a request, add the given XMl Document as the content of the request.
+     *
+     * @param document XMl document to send
+     * @return this, for method chaining
+     */
+    public ApiPathBuilder xml(final Document document) {
+        if (document != null) {
+            xmlDocument = document;
+        }
+        return this;
+    }
 
     /**
      * @return all attachments to be POSTed, with their names
@@ -311,7 +325,7 @@ class ApiPathBuilder {
      * Return true if there are any Attachments or Form data for a POST request.
      */
     public boolean hasPostContent() {
-        return getAttachments().size() > 0 || getForm().size() > 0;
+        return getAttachments().size() > 0 || getForm().size() > 0 || null != xmlDocument;
     }
 
     /**
@@ -321,6 +335,10 @@ class ApiPathBuilder {
         return accept;
     }
 
+    public Document getXmlDocument() {
+        return xmlDocument;
+    }
+
     /**
      * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder}
      *

From 7543d5d630eb51acd1e05b215bb187d3f5f23fa7 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 12:30:23 -0800
Subject: [PATCH 35/89] Send xml document if included in request

---
 src/main/java/org/rundeck/api/ApiCall.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index daac945..1dc2059 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -32,6 +32,7 @@ import org.apache.http.client.methods.HttpRequestBase;
 import org.apache.http.conn.scheme.Scheme;
 import org.apache.http.conn.ssl.SSLSocketFactory;
 import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.entity.EntityTemplate;
 import org.apache.http.entity.mime.HttpMultipartMode;
 import org.apache.http.entity.mime.MultipartEntity;
 import org.apache.http.entity.mime.content.InputStreamBody;
@@ -48,6 +49,7 @@ import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.parser.ParserHelper;
 import org.rundeck.api.parser.XmlNodeParser;
 import org.rundeck.api.util.AssertUtil;
+import org.rundeck.api.util.DocumentContentProducer;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
@@ -286,6 +288,9 @@ class ApiCall {
             } catch (UnsupportedEncodingException e) {
                 throw new RundeckApiException("Unsupported encoding: " + e.getMessage(), e);
             }
+        }else if(apiPath.getXmlDocument()!=null) {
+            httpPost.setHeader("Content-Type", "application/xml");
+            httpPost.setEntity(new EntityTemplate(new DocumentContentProducer(apiPath.getXmlDocument())));
         }else {
             throw new IllegalArgumentException("No Form or Multipart entity for POST content-body");
         }

From 303a3dec576f5c390855a95435d1b5b635856a7d Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 12:31:09 -0800
Subject: [PATCH 36/89] Generates project xml document for creation request

---
 .../api/generator/ProjectGenerator.java       | 38 +++++++++++++++++++
 .../api/generator/ProjectGeneratorTest.java   | 25 ++++++++++++
 2 files changed, 63 insertions(+)
 create mode 100644 src/main/java/org/rundeck/api/generator/ProjectGenerator.java
 create mode 100644 src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java

diff --git a/src/main/java/org/rundeck/api/generator/ProjectGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
new file mode 100644
index 0000000..37a45a4
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
@@ -0,0 +1,38 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+/**
+ * ProjectGenerator is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectGenerator {
+    RundeckProject project;
+
+    public ProjectGenerator(RundeckProject project) {
+        this.project = project;
+    }
+
+    public Document generate() {
+        Document projectDom = DocumentFactory.getInstance().createDocument();
+        Element rootElem = projectDom.addElement("project");
+        rootElem.addElement("name").setText(project.getName());
+        ProjectConfig configuration = project.getProjectConfig();
+        if (null != configuration) {
+
+            Element config = rootElem.addElement("config");
+            for (String s : configuration.getProperties().keySet()) {
+                Element property = config.addElement("property");
+                property.addAttribute("key", s);
+                property.addAttribute("value", configuration.getProperties().get(s));
+            }
+        }
+        return projectDom;
+    }
+}
diff --git a/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java b/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
new file mode 100644
index 0000000..a9c5b1b
--- /dev/null
+++ b/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
@@ -0,0 +1,25 @@
+package org.rundeck.api.generator;
+
+import junit.framework.Assert;
+import org.dom4j.Document;
+import org.junit.Test;
+import org.rundeck.api.domain.RundeckProject;
+
+/**
+ * ProjectGeneratorTest is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectGeneratorTest {
+    @Test
+    public void generate() {
+        RundeckProject project = new RundeckProject();
+        project.setName("monkey1");
+
+        Document doc = new ProjectGenerator(project).generate();
+        Assert.assertEquals("project", doc.getRootElement().getName());
+        Assert.assertNotNull(doc.selectSingleNode("/project/name"));
+        Assert.assertEquals("monkey1", doc.selectSingleNode("/project/name").getText());
+    }
+}

From b571c2a5beb1ca07bf7e12db7ad549777bf98d8b Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 12:31:42 -0800
Subject: [PATCH 37/89] Support API v11, and project create endpoint

---
 .../java/org/rundeck/api/RundeckClient.java   | 118 +++++++++++++-----
 .../org/rundeck/api/domain/ProjectConfig.java |  64 ++++++++++
 .../rundeck/api/domain/RundeckProject.java    |  23 +++-
 .../api/parser/ProjectConfigParser.java       |  43 +++++++
 .../org/rundeck/api/parser/ProjectParser.java |   5 +-
 .../rundeck/api/parser/ProjectParserV11.java  |  36 ++++++
 .../org/rundeck/api/RundeckClientTest.java    |  10 ++
 .../api/parser/ProjectConfigParserTest.java   |  51 ++++++++
 .../api/parser/ProjectParserV11Test.java      |  28 +++++
 .../betamax/tapes/create_projectv11.yaml      |  26 ++++
 .../org/rundeck/api/parser/projectv11.xml     |  17 +++
 11 files changed, 388 insertions(+), 33 deletions(-)
 create mode 100644 src/main/java/org/rundeck/api/domain/ProjectConfig.java
 create mode 100644 src/main/java/org/rundeck/api/parser/ProjectConfigParser.java
 create mode 100644 src/main/java/org/rundeck/api/parser/ProjectParserV11.java
 create mode 100644 src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java
 create mode 100644 src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java
 create mode 100644 src/test/resources/betamax/tapes/create_projectv11.yaml
 create mode 100644 src/test/resources/org/rundeck/api/parser/projectv11.xml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 61911bc..57ae3af 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -18,10 +18,14 @@ package org.rundeck.api;
 import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
 import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
 import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.domain.*;
 import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
+import org.rundeck.api.generator.ProjectGenerator;
 import org.rundeck.api.parser.*;
 import org.rundeck.api.query.ExecutionQuery;
 import org.rundeck.api.util.AssertUtil;
@@ -33,10 +37,7 @@ import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.Serializable;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Properties;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -95,6 +96,7 @@ public class RundeckClient implements Serializable {
         V8(8),
         V9(9),
         V10(10),
+        V11(11),
         ;
 
         private int versionNumber;
@@ -108,7 +110,7 @@ public class RundeckClient implements Serializable {
         }
     }
     /** Version of the API supported */
-    public static final transient int API_VERSION = Version.V10.getVersionNumber();
+    public static final transient int API_VERSION = Version.V11.getVersionNumber();
 
     private static final String API = "/api/";
 
@@ -274,10 +276,26 @@ public class RundeckClient implements Serializable {
         testAuth();
     }
 
+    /**
+     * Return root xpath for xml api results. for v11 and later it is empty, for earlier it is "result"
+     *
+     * @return
+     */
+    private String rootXpath() {
+        return getApiVersion() < Version.V11.getVersionNumber() ? "result" : "";
+    }
     /*
      * Projects
      */
 
+    private ProjectParser createProjectParser() {
+        return createProjectParser(null);
+    }
+
+    private ProjectParser createProjectParser(final String xpath) {
+        return new ProjectParserV11(xpath);
+    }
+
     /**
      * List all projects
      *
@@ -289,7 +307,8 @@ public class RundeckClient implements Serializable {
     public List getProjects() throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException {
         return new ApiCall(this).get(new ApiPathBuilder("/projects"),
-                                     new ListParser(new ProjectParser(), "result/projects/project"));
+                                     new ListParser(createProjectParser(), rootXpath() +
+                                             "/projects/project"));
     }
 
     /**
@@ -306,7 +325,48 @@ public class RundeckClient implements Serializable {
             RundeckApiTokenException, IllegalArgumentException {
         AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !");
         return new ApiCall(this).get(new ApiPathBuilder("/project/", projectName),
-                                     new ProjectParser("result/projects/project"));
+                createProjectParser(rootXpath() +
+                        (getApiVersion() < Version.V11.getVersionNumber()
+                                ? "/projects/project"
+                                : "/project"
+                        )));
+    }
+
+    /**
+     * Create a new project, and return the new definition
+     *
+     * @param projectName name of the project - mandatory
+     * @param configuration project configuration properties
+     *
+     * @return a {@link RundeckProject} instance - won't be null
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public RundeckProject createProject(String projectName, Map configuration) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !");
+        return new ApiCall(this)
+                .post(new ApiPathBuilder("/projects").xml(
+                        projectDocument(projectName, configuration)
+                ), createProjectParser(rootXpath() +
+                        (getApiVersion() < Version.V11.getVersionNumber()
+                                ? "/projects/project"
+                                : "/project"
+                        )));
+    }
+
+    private Document projectDocument(String projectName, Map configuration) {
+        RundeckProject project = new RundeckProject();
+        project.setName(projectName);
+        if (null != configuration) {
+            project.setProjectConfig(new ProjectConfig(configuration));
+        }
+        return new ProjectGenerator(project).generate();
     }
 
     /*
@@ -366,7 +426,7 @@ public class RundeckClient implements Serializable {
                                                                 .param("jobFilter", jobFilter)
                                                                 .param("groupPath", groupPath)
                                                                 .param("idlist", StringUtils.join(jobIds, ",")),
-                                     new ListParser(new JobParser(), "result/jobs/job"));
+                                     new ListParser(new JobParser(), rootXpath()+"/jobs/job"));
     }
 
     /**
@@ -699,7 +759,7 @@ public class RundeckClient implements Serializable {
             //API v8
             request.param("project", rundeckJobsImport.getProject());
         }
-        return new ApiCall(this).post(request, new JobsImportResultParser("result"));
+        return new ApiCall(this).post(request, new JobsImportResultParser(rootXpath()));
     }
 
     /**
@@ -755,7 +815,7 @@ public class RundeckClient implements Serializable {
     public String deleteJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException, IllegalArgumentException {
         AssertUtil.notBlank(jobId, "jobId is mandatory to delete a job !");
-        return new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId), new StringParser("result/success/message"));
+        return new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId), new StringParser(rootXpath()+"/success/message"));
     }
     /**
      * Delete multiple jobs, identified by the given IDs
@@ -773,7 +833,7 @@ public class RundeckClient implements Serializable {
             throw new IllegalArgumentException("jobIds are mandatory to delete a job");
         }
         return new ApiCall(this).post(new ApiPathBuilder("/jobs/delete").field("ids",jobIds),
-                                        new BulkDeleteParser("result/deleteJobs"));
+                                        new BulkDeleteParser(rootXpath()+"/deleteJobs"));
     }
 
     /**
@@ -798,7 +858,7 @@ public class RundeckClient implements Serializable {
         if(null!=jobRun.getAsUser()) {
             apiPath.param("asUser", jobRun.getAsUser());
         }
-        return new ApiCall(this).get(apiPath, new ExecutionParser("result/executions/execution"));
+        return new ApiCall(this).get(apiPath, new ExecutionParser(rootXpath()+"/executions/execution"));
     }
 
 
@@ -896,7 +956,7 @@ public class RundeckClient implements Serializable {
         if(null!= command.getAsUser()) {
             apiPath.param("asUser", command.getAsUser());
         }
-        RundeckExecution execution = new ApiCall(this).get(apiPath, new ExecutionParser("result/execution"));
+        RundeckExecution execution = new ApiCall(this).get(apiPath, new ExecutionParser(rootXpath()+"/execution"));
         // the first call just returns the ID of the execution, so we need another call to get a "real" execution
         return getExecution(execution.getId());
     }
@@ -1030,7 +1090,7 @@ public class RundeckClient implements Serializable {
         if(null!=script.getAsUser()) {
             apiPath.param("asUser", script.getAsUser());
         }
-        RundeckExecution execution = new ApiCall(this).post(apiPath, new ExecutionParser("result/execution"));
+        RundeckExecution execution = new ApiCall(this).post(apiPath, new ExecutionParser(rootXpath()+"/execution"));
         // the first call just returns the ID of the execution, so we need another call to get a "real" execution
         return getExecution(execution.getId());
     }
@@ -1177,7 +1237,7 @@ public class RundeckClient implements Serializable {
         AssertUtil.notBlank(project, "project is mandatory get all running executions !");
         return new ApiCall(this).get(new ApiPathBuilder("/executions/running").param("project", project),
                                      new ListParser(new ExecutionParser(),
-                                                                      "result/executions/execution"));
+                                                                      rootXpath()+"/executions/execution"));
     }
 
     /**
@@ -1275,7 +1335,7 @@ public class RundeckClient implements Serializable {
                                          .param("max", max)
                                          .param("offset", offset),
                                      new ListParser(new ExecutionParser(),
-                                                                      "result/executions/execution"));
+                                                                      rootXpath()+"/executions/execution"));
     }
 
     /**
@@ -1302,7 +1362,7 @@ public class RundeckClient implements Serializable {
                                          .param("offset", offset),
                                      new PagedResultParser(
                                          new ListParser(new ExecutionParser(), "execution"),
-                                         "result/executions"
+                                         rootXpath()+"/executions"
                                      )
         );
     }
@@ -1321,7 +1381,7 @@ public class RundeckClient implements Serializable {
             RundeckApiTokenException, IllegalArgumentException {
         AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !");
         return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString()),
-                                     new ExecutionParser("result/executions/execution"));
+                                     new ExecutionParser(rootXpath()+"/executions/execution"));
     }
 
     /**
@@ -1356,7 +1416,7 @@ public class RundeckClient implements Serializable {
         if(null!=asUser) {
             apiPath.param("asUser", asUser);
         }
-        return new ApiCall(this).get(apiPath, new AbortParser("result/abort"));
+        return new ApiCall(this).get(apiPath, new AbortParser(rootXpath()+"/abort"));
     }
 
     /*
@@ -1548,7 +1608,7 @@ public class RundeckClient implements Serializable {
                                          .param("end", end)
                                          .param("max", max)
                                          .param("offset", offset),
-                                     new HistoryParser("result/events"));
+                                     new HistoryParser(rootXpath()+"/events"));
     }
 
     /**
@@ -1595,7 +1655,7 @@ public class RundeckClient implements Serializable {
             .param("max", max)
             .param("offset", offset);
 
-        return new ApiCall(this).postOrGet(builder, new HistoryParser("result/events"));
+        return new ApiCall(this).postOrGet(builder, new HistoryParser(rootXpath()+"/events"));
     }
 
     /*
@@ -1773,7 +1833,7 @@ public class RundeckClient implements Serializable {
             param.param("maxlines", maxlines);
         }
         return new ApiCall(this).get(param,
-                new OutputParser("result/output", createOutputEntryParser()));
+                new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
     }
     /**
      * Get the execution state of the given execution
@@ -1792,7 +1852,7 @@ public class RundeckClient implements Serializable {
                 "/execution/", executionId.toString(),
                 "/state");
 
-        return new ApiCall(this).get(param, new ExecutionStateParser("result/executionState"));
+        return new ApiCall(this).get(param, new ExecutionStateParser(rootXpath()+"/executionState"));
     }
 
     /**
@@ -1834,7 +1894,7 @@ public class RundeckClient implements Serializable {
             param.param("maxlines", maxlines);
         }
         return new ApiCall(this).get(param,
-                new OutputParser("result/output", createOutputEntryParser()));
+                new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
     }
     /**
      * Get the execution output of the given execution for the specified step
@@ -1875,7 +1935,7 @@ public class RundeckClient implements Serializable {
             param.param("maxlines", maxlines);
         }
         return new ApiCall(this).get(param,
-                new OutputParser("result/output", createOutputEntryParser()));
+                new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
     }
     /**
      * Get the execution output of the given execution for the specified step
@@ -1919,7 +1979,7 @@ public class RundeckClient implements Serializable {
             param.param("maxlines", maxlines);
         }
         return new ApiCall(this).get(param,
-                new OutputParser("result/output", createOutputEntryParser()));
+                new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
     }
 
 
@@ -1966,7 +2026,7 @@ public class RundeckClient implements Serializable {
         if (maxlines > 0) {
             param.param("maxlines", maxlines);
         }
-        return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser()));
+        return new ApiCall(this).get(param, new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
     }
     /**
      * Get the execution state output sequence of the given job
@@ -1997,7 +2057,7 @@ public class RundeckClient implements Serializable {
         if(stateOnly) {
             param.param("stateOnly", true);
         }
-        return new ApiCall(this).get(param, new OutputParser("result/output", createOutputEntryParser()));
+        return new ApiCall(this).get(param, new OutputParser(rootXpath()+"/output", createOutputEntryParser()));
     }
 
     private OutputEntryParser createOutputEntryParser() {
@@ -2023,7 +2083,7 @@ public class RundeckClient implements Serializable {
      */
     public RundeckSystemInfo getSystemInfo() throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException {
-        return new ApiCall(this).get(new ApiPathBuilder("/system/info"), new SystemInfoParser("result/system"));
+        return new ApiCall(this).get(new ApiPathBuilder("/system/info"), new SystemInfoParser(rootXpath()+"/system"));
     }
 
     /**
diff --git a/src/main/java/org/rundeck/api/domain/ProjectConfig.java b/src/main/java/org/rundeck/api/domain/ProjectConfig.java
new file mode 100644
index 0000000..ca3548f
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/ProjectConfig.java
@@ -0,0 +1,64 @@
+package org.rundeck.api.domain;
+
+import java.io.Serializable;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * ProjectConfig is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfig implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    private LinkedHashMap properties = new LinkedHashMap();
+
+    public ProjectConfig() {
+    }
+
+    public ProjectConfig(Map properties) {
+        setProperties(properties);
+    }
+
+    public void setProperty(final String key, final String value) {
+        getProperties().put(key, value);
+    }
+
+    public void addProperties(final Map values) {
+        getProperties().putAll(values);
+    }
+
+    public Map getProperties() {
+        return properties;
+    }
+
+    public void setProperties(final Map properties) {
+        this.properties = new LinkedHashMap(properties);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ProjectConfig)) return false;
+
+        ProjectConfig that = (ProjectConfig) o;
+
+        if (properties != null ? !properties.equals(that.properties) : that.properties != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return properties != null ? properties.hashCode() : 0;
+    }
+
+    @Override
+    public String toString() {
+        return "ProjectConfig{" +
+                "properties=" + properties +
+                '}';
+    }
+}
diff --git a/src/main/java/org/rundeck/api/domain/RundeckProject.java b/src/main/java/org/rundeck/api/domain/RundeckProject.java
index c5e3fa4..32e38ea 100644
--- a/src/main/java/org/rundeck/api/domain/RundeckProject.java
+++ b/src/main/java/org/rundeck/api/domain/RundeckProject.java
@@ -32,6 +32,8 @@ public class RundeckProject implements Serializable {
 
     private String resourceModelProviderUrl;
 
+    private ProjectConfig projectConfig;
+
     public String getName() {
         return name;
     }
@@ -56,10 +58,20 @@ public class RundeckProject implements Serializable {
         this.resourceModelProviderUrl = resourceModelProviderUrl;
     }
 
+    public ProjectConfig getProjectConfig() {
+        return projectConfig;
+    }
+
+    public void setProjectConfig(ProjectConfig projectConfig) {
+        this.projectConfig = projectConfig;
+    }
+
     @Override
     public String toString() {
-        return "RundeckProject [name=" + name + ", description=" + description + ", resourceModelProviderUrl="
-               + resourceModelProviderUrl + "]";
+        return "RundeckProject [name=" + name + ", description=" + description
+                + (null!=resourceModelProviderUrl? ", resourceModelProviderUrl=" + resourceModelProviderUrl : "")
+                + ", config="
+               + projectConfig + "]";
     }
 
     @Override
@@ -69,6 +81,7 @@ public class RundeckProject implements Serializable {
         result = prime * result + ((description == null) ? 0 : description.hashCode());
         result = prime * result + ((name == null) ? 0 : name.hashCode());
         result = prime * result + ((resourceModelProviderUrl == null) ? 0 : resourceModelProviderUrl.hashCode());
+        result = prime * result + ((projectConfig == null) ? 0 : projectConfig.hashCode());
         return result;
     }
 
@@ -96,7 +109,11 @@ public class RundeckProject implements Serializable {
                 return false;
         } else if (!resourceModelProviderUrl.equals(other.resourceModelProviderUrl))
             return false;
+        if (projectConfig == null) {
+            if (other.projectConfig != null)
+                return false;
+        } else if (!projectConfig.equals(other.projectConfig))
+            return false;
         return true;
     }
-
 }
diff --git a/src/main/java/org/rundeck/api/parser/ProjectConfigParser.java b/src/main/java/org/rundeck/api/parser/ProjectConfigParser.java
new file mode 100644
index 0000000..7e92ffe
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ProjectConfigParser.java
@@ -0,0 +1,43 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ProjectConfig;
+
+import java.util.List;
+
+/**
+ * ProjectConfigParser parses project "config" element contents
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigParser implements XmlNodeParser {
+    private String xpath;
+
+    public ProjectConfigParser() {
+    }
+
+    public ProjectConfigParser(String xpath) {
+        this.xpath = xpath;
+    }
+
+    @Override
+    public ProjectConfig parseXmlNode(Node node) {
+        Node config1 = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
+        ProjectConfig config = new ProjectConfig();
+        List property = config1.selectNodes("property");
+        for (Object o : property) {
+            Node propnode = (Node) o;
+            String key = propnode.valueOf("@key");
+            String value = propnode.valueOf("@value");
+            if (null != key && null != value) {
+                config.setProperty(key, value);
+            }
+        }
+        return config;
+    }
+
+    public String getXpath() {
+        return xpath;
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/ProjectParser.java b/src/main/java/org/rundeck/api/parser/ProjectParser.java
index 999ec70..ef6ca13 100644
--- a/src/main/java/org/rundeck/api/parser/ProjectParser.java
+++ b/src/main/java/org/rundeck/api/parser/ProjectParser.java
@@ -42,7 +42,7 @@ public class ProjectParser implements XmlNodeParser {
 
     @Override
     public RundeckProject parseXmlNode(Node node) {
-        Node projectNode = xpath != null ? node.selectSingleNode(xpath) : node;
+        Node projectNode = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
 
         RundeckProject project = new RundeckProject();
 
@@ -53,4 +53,7 @@ public class ProjectParser implements XmlNodeParser {
         return project;
     }
 
+    protected String getXpath() {
+        return xpath;
+    }
 }
diff --git a/src/main/java/org/rundeck/api/parser/ProjectParserV11.java b/src/main/java/org/rundeck/api/parser/ProjectParserV11.java
new file mode 100644
index 0000000..631d941
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ProjectParserV11.java
@@ -0,0 +1,36 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.util.List;
+
+/**
+ * ProjectParserV11 supports embedded "config" element.
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectParserV11 extends ProjectParser {
+    public ProjectParserV11() {
+    }
+
+    public ProjectParserV11(final String xpath) {
+        super(xpath);
+    }
+
+    @Override
+    public RundeckProject parseXmlNode(final Node node) {
+        final RundeckProject rundeckProject = super.parseXmlNode(node);
+        final Node projectNode = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
+        final Node config1 = projectNode.selectSingleNode("config");
+        if (config1 == null) {
+            return rundeckProject;
+        }
+
+        rundeckProject.setProjectConfig(new ProjectConfigParser().parseXmlNode(config1));
+
+        return rundeckProject;
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 6e755db..c66ea7c 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -82,6 +82,16 @@ public class RundeckClientTest {
         Assert.assertNull(projects.get(0).getDescription());
     }
     @Test
+    @Betamax(tape = "create_projectv11")
+    public void createProject() throws Exception {
+
+        RundeckProject project = createClient(TEST_TOKEN_6,11).createProject("monkey1", null);
+        Assert.assertEquals("monkey1", project.getName());
+        Assert.assertEquals(null, project.getDescription());
+        Assert.assertNotNull(project.getProjectConfig());
+
+    }
+    @Test
     @Betamax(tape = "get_history")
     public void getHistory() throws Exception {
         final RundeckHistory test = client.getHistory("test");
diff --git a/src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java b/src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java
new file mode 100644
index 0000000..cee0f6f
--- /dev/null
+++ b/src/test/java/org/rundeck/api/parser/ProjectConfigParserTest.java
@@ -0,0 +1,51 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Document;
+import org.junit.Assert;
+import org.junit.Test;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.io.InputStream;
+
+/**
+ * ProjectConfigParserTest is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigParserTest {
+    @Test
+    public void parseProject() throws Exception {
+        InputStream input = getClass().getResourceAsStream("projectv11.xml");
+        Document document = ParserHelper.loadDocument(input);
+
+        ProjectConfig config = new ProjectConfigParser("project/config").parseXmlNode(document);
+
+        Assert.assertEquals(10, config.getProperties().size());
+        Assert.assertEquals("ziggy", config.getProperties().get("project.name"));
+        Assert.assertEquals("false", config.getProperties().get("resources.source.1.config.requireFileExists"));
+        Assert.assertEquals("privateKey", config.getProperties().get("project.ssh-authentication"));
+        Assert.assertEquals("jsch-ssh", config.getProperties().get("service.NodeExecutor.default.provider"));
+        Assert.assertEquals("false", config.getProperties().get("resources.source.1.config.includeServerNode"));
+        Assert.assertEquals("false", config.getProperties().get("resources.source.1.config.generateFileAutomatically"));
+        Assert.assertEquals("/var/rundeck/projects/${project.name}/etc/resources.xml",
+                config.getProperties().get("resources.source.1.config.file"));
+        Assert.assertEquals("/var/lib/rundeck/.ssh/id_rsa", config.getProperties().get("project.ssh-keypath"));
+        Assert.assertEquals("jsch-scp", config.getProperties().get("service.FileCopier.default.provider"));
+        Assert.assertEquals("file", config.getProperties().get("resources.source.1.type"));
+        /*
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+         */
+    }
+}
diff --git a/src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java b/src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java
new file mode 100644
index 0000000..cff9fc9
--- /dev/null
+++ b/src/test/java/org/rundeck/api/parser/ProjectParserV11Test.java
@@ -0,0 +1,28 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Document;
+import org.junit.Assert;
+import org.junit.Test;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.io.InputStream;
+
+/**
+ * ProjectParserV11Test is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectParserV11Test {
+    @Test
+    public void parseProject() throws Exception {
+        InputStream input = getClass().getResourceAsStream("projectv11.xml");
+        Document document = ParserHelper.loadDocument(input);
+
+        RundeckProject project = new ProjectParserV11("project").parseXmlNode(document);
+
+        Assert.assertEquals("ziggy", project.getName());
+        Assert.assertNull(project.getDescription());
+        Assert.assertNotNull(project.getProjectConfig());
+    }
+}
diff --git a/src/test/resources/betamax/tapes/create_projectv11.yaml b/src/test/resources/betamax/tapes/create_projectv11.yaml
new file mode 100644
index 0000000..e928798
--- /dev/null
+++ b/src/test/resources/betamax/tapes/create_projectv11.yaml
@@ -0,0 +1,26 @@
+!tape
+name: create_projectv11
+interactions:
+- recorded: 2014-02-27T19:45:35.430Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/11/projects
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1h2r5l35j4ynqo19dsm6xa2gv;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHByb2plY3QgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3Byb2plY3QvbW9ua2V5MSc+CiAgPG5hbWU+bW9ua2V5MTwvbmFtZT4KICA8ZGVzY3JpcHRpb24+PC9kZXNjcmlwdGlvbj4KICA8Y29uZmlnPgogICAgPHByb3BlcnR5IGtleT0ncHJvamVjdC5uYW1lJyB2YWx1ZT0nbW9ua2V5MScgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Byb2plY3Quc3NoLWF1dGhlbnRpY2F0aW9uJyB2YWx1ZT0ncHJpdmF0ZUtleScgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3NlcnZpY2UuTm9kZUV4ZWN1dG9yLmRlZmF1bHQucHJvdmlkZXInIHZhbHVlPSdqc2NoLXNzaCcgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Jlc291cmNlcy5zb3VyY2UuMS5jb25maWcuaW5jbHVkZVNlcnZlck5vZGUnIHZhbHVlPSd0cnVlJyAvPgogICAgPHByb3BlcnR5IGtleT0ncmVzb3VyY2VzLnNvdXJjZS4xLmNvbmZpZy5nZW5lcmF0ZUZpbGVBdXRvbWF0aWNhbGx5JyB2YWx1ZT0ndHJ1ZScgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Jlc291cmNlcy5zb3VyY2UuMS5jb25maWcuZmlsZScgdmFsdWU9Jy9Vc2Vycy9ncmVnL3J1bmRlY2syZC9wcm9qZWN0cy9tb25rZXkxL2V0Yy9yZXNvdXJjZXMueG1sJyAvPgogICAgPHByb3BlcnR5IGtleT0ncHJvamVjdC5zc2gta2V5cGF0aCcgdmFsdWU9Jy9Vc2Vycy9ncmVnLy5zc2gvaWRfcnNhJyAvPgogICAgPHByb3BlcnR5IGtleT0nc2VydmljZS5GaWxlQ29waWVyLmRlZmF1bHQucHJvdmlkZXInIHZhbHVlPSdqc2NoLXNjcCcgLz4KICAgIDxwcm9wZXJ0eSBrZXk9J3Jlc291cmNlcy5zb3VyY2UuMS50eXBlJyB2YWx1ZT0nZmlsZScgLz4KICA8L2NvbmZpZz4KPC9wcm9qZWN0Pg==
diff --git a/src/test/resources/org/rundeck/api/parser/projectv11.xml b/src/test/resources/org/rundeck/api/parser/projectv11.xml
new file mode 100644
index 0000000..e47e69d
--- /dev/null
+++ b/src/test/resources/org/rundeck/api/parser/projectv11.xml
@@ -0,0 +1,17 @@
+
+    ziggy
+    
+    
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+    
+

From 92eb7acc1531c36711ed3d7672803d3cb30fa7c8 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 27 Feb 2014 12:44:26 -0800
Subject: [PATCH 38/89] Add get Project/config endpoint

---
 .../java/org/rundeck/api/RundeckClient.java   | 22 ++++++++++++++++-
 .../org/rundeck/api/RundeckClientTest.java    | 11 +++++++++
 .../betamax/tapes/get_project_configv11.yaml  | 24 +++++++++++++++++++
 3 files changed, 56 insertions(+), 1 deletion(-)
 create mode 100644 src/test/resources/betamax/tapes/get_project_configv11.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 57ae3af..cd3739e 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -349,7 +349,7 @@ public class RundeckClient implements Serializable {
             RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException, IllegalArgumentException {
 
-        AssertUtil.notBlank(projectName, "projectName is mandatory to get the details of a project !");
+        AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
         return new ApiCall(this)
                 .post(new ApiPathBuilder("/projects").xml(
                         projectDocument(projectName, configuration)
@@ -359,6 +359,26 @@ public class RundeckClient implements Serializable {
                                 : "/project"
                         )));
     }
+    /**
+     * Return the configuration of a project
+     *
+     * @param projectName name of the project - mandatory
+     *
+     * @return a {@link ProjectConfig} instance - won't be null
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public ProjectConfig getProjectConfig(String projectName) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
+        return new ApiCall(this)
+                .get(new ApiPathBuilder("/project/", projectName, "/config"), new ProjectConfigParser("/config"));
+    }
 
     private Document projectDocument(String projectName, Map configuration) {
         RundeckProject project = new RundeckProject();
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index c66ea7c..4ab1fe7 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -91,6 +91,17 @@ public class RundeckClientTest {
         Assert.assertNotNull(project.getProjectConfig());
 
     }
+
+    @Test
+    @Betamax(tape = "get_project_configv11")
+    public void getProjectConfig() throws Exception {
+        ProjectConfig config = createClient(TEST_TOKEN_6, 11).getProjectConfig("monkey1");
+        Assert.assertNotNull(config);
+        Assert.assertNotNull(config.getProperties());
+        Assert.assertEquals(9,config.getProperties().size());
+        Assert.assertEquals("monkey1", config.getProperties().get("project.name"));
+    }
+
     @Test
     @Betamax(tape = "get_history")
     public void getHistory() throws Exception {
diff --git a/src/test/resources/betamax/tapes/get_project_configv11.yaml b/src/test/resources/betamax/tapes/get_project_configv11.yaml
new file mode 100644
index 0000000..033aa13
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_project_configv11.yaml
@@ -0,0 +1,24 @@
+!tape
+name: get_project_configv11
+interactions:
+- recorded: 2014-02-27T20:35:47.282Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/project/monkey1/config
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=bo96n10n268hsd1gi9y67nah;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGNvbmZpZz4KICA8cHJvcGVydHkga2V5PSdwcm9qZWN0Lm5hbWUnIHZhbHVlPSdtb25rZXkxJyAvPgogIDxwcm9wZXJ0eSBrZXk9J3Byb2plY3Quc3NoLWF1dGhlbnRpY2F0aW9uJyB2YWx1ZT0ncHJpdmF0ZUtleScgLz4KICA8cHJvcGVydHkga2V5PSdzZXJ2aWNlLk5vZGVFeGVjdXRvci5kZWZhdWx0LnByb3ZpZGVyJyB2YWx1ZT0nanNjaC1zc2gnIC8+CiAgPHByb3BlcnR5IGtleT0ncmVzb3VyY2VzLnNvdXJjZS4xLmNvbmZpZy5pbmNsdWRlU2VydmVyTm9kZScgdmFsdWU9J3RydWUnIC8+CiAgPHByb3BlcnR5IGtleT0ncmVzb3VyY2VzLnNvdXJjZS4xLmNvbmZpZy5nZW5lcmF0ZUZpbGVBdXRvbWF0aWNhbGx5JyB2YWx1ZT0ndHJ1ZScgLz4KICA8cHJvcGVydHkga2V5PSdyZXNvdXJjZXMuc291cmNlLjEuY29uZmlnLmZpbGUnIHZhbHVlPScvVXNlcnMvZ3JlZy9ydW5kZWNrMmQvcHJvamVjdHMvbW9ua2V5MS9ldGMvcmVzb3VyY2VzLnhtbCcgLz4KICA8cHJvcGVydHkga2V5PSdwcm9qZWN0LnNzaC1rZXlwYXRoJyB2YWx1ZT0nL1VzZXJzL2dyZWcvLnNzaC9pZF9yc2EnIC8+CiAgPHByb3BlcnR5IGtleT0nc2VydmljZS5GaWxlQ29waWVyLmRlZmF1bHQucHJvdmlkZXInIHZhbHVlPSdqc2NoLXNjcCcgLz4KICA8cHJvcGVydHkga2V5PSdyZXNvdXJjZXMuc291cmNlLjEudHlwZScgdmFsdWU9J2ZpbGUnIC8+CjwvY29uZmlnPg==

From e548c14b242f393406fb46f22b276d085b7366d2 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Mar 2014 10:07:31 -0800
Subject: [PATCH 39/89] Add setProjectConfig for apiv11

---
 src/main/java/org/rundeck/api/ApiCall.java    | 36 ++++++++++------
 .../java/org/rundeck/api/ApiPathBuilder.java  | 13 ++++++
 .../java/org/rundeck/api/RundeckClient.java   | 25 ++++++++++-
 .../api/generator/BaseDocGenerator.java       | 18 ++++++++
 .../api/generator/ProjectConfigGenerator.java | 34 +++++++++++++++
 .../api/generator/ProjectGenerator.java       | 18 +++-----
 .../api/generator/XmlDocumentGenerator.java   | 29 +++++++++++++
 .../org/rundeck/api/RundeckClientTest.java    | 14 +++++++
 .../generator/ProjectConfigGeneratorTest.java | 41 +++++++++++++++++++
 .../api/generator/ProjectGeneratorTest.java   |  2 +-
 .../betamax/tapes/set_project_configv11.yaml  | 26 ++++++++++++
 11 files changed, 230 insertions(+), 26 deletions(-)
 create mode 100644 src/main/java/org/rundeck/api/generator/BaseDocGenerator.java
 create mode 100644 src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java
 create mode 100644 src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java
 create mode 100644 src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java
 create mode 100644 src/test/resources/betamax/tapes/set_project_configv11.yaml

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index 1dc2059..8d9f857 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -16,19 +16,10 @@
 package org.rundeck.api;
 
 import org.apache.commons.lang.StringUtils;
-import org.apache.http.Header;
-import org.apache.http.HttpException;
-import org.apache.http.HttpRequest;
-import org.apache.http.HttpRequestInterceptor;
-import org.apache.http.HttpResponse;
-import org.apache.http.NameValuePair;
-import org.apache.http.ParseException;
+import org.apache.http.*;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.entity.UrlEncodedFormEntity;
-import org.apache.http.client.methods.HttpDelete;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.*;
 import org.apache.http.conn.scheme.Scheme;
 import org.apache.http.conn.ssl.SSLSocketFactory;
 import org.apache.http.conn.ssl.TrustStrategy;
@@ -272,6 +263,27 @@ class ApiCall {
     public  T post(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
             RundeckApiLoginException, RundeckApiTokenException {
         HttpPost httpPost = new HttpPost(client.getUrl() + client.getApiEndpoint() + apiPath);
+        return requestWithEntity(apiPath, parser, httpPost);
+    }
+    /**
+     * Execute an HTTP PUT request to the RunDeck instance, on the given path. We will login first, and then execute
+     * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
+     *
+     * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
+     * @param parser used to parse the response
+     * @return the result of the call, as formatted by the parser
+     * @throws RundeckApiException in case of error when calling the API
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     */
+    public  T put(ApiPathBuilder apiPath, XmlNodeParser parser) throws RundeckApiException,
+            RundeckApiLoginException, RundeckApiTokenException {
+        HttpPut httpPut = new HttpPut(client.getUrl() + client.getApiEndpoint() + apiPath);
+        return requestWithEntity(apiPath, parser, httpPut);
+    }
+
+    private  T requestWithEntity(ApiPathBuilder apiPath, XmlNodeParser parser, HttpEntityEnclosingRequestBase
+            httpPost) {
         if(null!= apiPath.getAccept()) {
             httpPost.setHeader("Accept", apiPath.getAccept());
         }
@@ -344,7 +356,7 @@ class ApiCall {
      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
      */
-    private ByteArrayInputStream execute(HttpRequestBase request) throws RundeckApiException, RundeckApiLoginException,
+    private ByteArrayInputStream execute(HttpUriRequest request) throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException {
         HttpClient httpClient = instantiateHttpClient();
         try {
diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java
index e749004..bf27ae9 100644
--- a/src/main/java/org/rundeck/api/ApiPathBuilder.java
+++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java
@@ -27,6 +27,7 @@ import org.apache.commons.lang.StringUtils;
 import org.apache.http.NameValuePair;
 import org.apache.http.message.BasicNameValuePair;
 import org.dom4j.Document;
+import org.rundeck.api.generator.XmlDocumentGenerator;
 import org.rundeck.api.util.ParametersUtil;
 
 /**
@@ -280,6 +281,18 @@ class ApiPathBuilder {
         }
         return this;
     }
+    /**
+     * When POSTing a request, add the given XMl Document as the content of the request.
+     *
+     * @param document XMl document to send
+     * @return this, for method chaining
+     */
+    public ApiPathBuilder xml(final XmlDocumentGenerator document) {
+        if (document != null) {
+            xmlDocument = document.generateXmlDocument();
+        }
+        return this;
+    }
 
     /**
      * @return all attachments to be POSTed, with their names
diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index cd3739e..8cf1989 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -25,6 +25,7 @@ import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
 import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.domain.*;
 import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
+import org.rundeck.api.generator.ProjectConfigGenerator;
 import org.rundeck.api.generator.ProjectGenerator;
 import org.rundeck.api.parser.*;
 import org.rundeck.api.query.ExecutionQuery;
@@ -379,6 +380,28 @@ public class RundeckClient implements Serializable {
         return new ApiCall(this)
                 .get(new ApiPathBuilder("/project/", projectName, "/config"), new ProjectConfigParser("/config"));
     }
+    /**
+     * Return the configuration of a project
+     *
+     * @param projectName name of the project - mandatory
+     *
+     * @return a {@link ProjectConfig} instance - won't be null
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public ProjectConfig setProjectConfig(String projectName, Map configuration) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
+        return new ApiCall(this)
+                .put(new ApiPathBuilder("/project/", projectName, "/config")
+                        .xml(new ProjectConfigGenerator(new ProjectConfig(configuration)))
+                        , new ProjectConfigParser("/config"));
+    }
 
     private Document projectDocument(String projectName, Map configuration) {
         RundeckProject project = new RundeckProject();
@@ -386,7 +409,7 @@ public class RundeckClient implements Serializable {
         if (null != configuration) {
             project.setProjectConfig(new ProjectConfig(configuration));
         }
-        return new ProjectGenerator(project).generate();
+        return new ProjectGenerator(project).generateXmlDocument();
     }
 
     /*
diff --git a/src/main/java/org/rundeck/api/generator/BaseDocGenerator.java b/src/main/java/org/rundeck/api/generator/BaseDocGenerator.java
new file mode 100644
index 0000000..46bf574
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/BaseDocGenerator.java
@@ -0,0 +1,18 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+
+/**
+ * BaseDocGenerator generates a document using the element as the root.
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public abstract class BaseDocGenerator implements XmlDocumentGenerator {
+    @Override
+    public Document generateXmlDocument() {
+        return DocumentFactory.getInstance().createDocument(generateXmlElement());
+    }
+
+}
diff --git a/src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java
new file mode 100644
index 0000000..547b154
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectConfigGenerator.java
@@ -0,0 +1,34 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ProjectConfig;
+
+/**
+ * ProjectConfigGenerator is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigGenerator extends BaseDocGenerator {
+    private ProjectConfig config;
+
+    public ProjectConfigGenerator(ProjectConfig config) {
+        this.config = config;
+    }
+
+    @Override
+    public Element generateXmlElement() {
+        Element configEl = DocumentFactory.getInstance().createElement("config");
+        if (null != config.getProperties()) {
+            for (String s : config.getProperties().keySet()) {
+                Element property = configEl.addElement("property");
+                property.addAttribute("key", s);
+                property.addAttribute("value", config.getProperties().get(s));
+            }
+        }
+        return configEl;
+    }
+
+}
diff --git a/src/main/java/org/rundeck/api/generator/ProjectGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
index 37a45a4..8960acf 100644
--- a/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
+++ b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
@@ -12,27 +12,21 @@ import org.rundeck.api.domain.RundeckProject;
  * @author greg
  * @since 2014-02-27
  */
-public class ProjectGenerator {
+public class ProjectGenerator extends BaseDocGenerator {
     RundeckProject project;
 
     public ProjectGenerator(RundeckProject project) {
         this.project = project;
     }
 
-    public Document generate() {
-        Document projectDom = DocumentFactory.getInstance().createDocument();
-        Element rootElem = projectDom.addElement("project");
+    @Override
+    public Element generateXmlElement() {
+        Element rootElem = DocumentFactory.getInstance().createElement("project");
         rootElem.addElement("name").setText(project.getName());
         ProjectConfig configuration = project.getProjectConfig();
         if (null != configuration) {
-
-            Element config = rootElem.addElement("config");
-            for (String s : configuration.getProperties().keySet()) {
-                Element property = config.addElement("property");
-                property.addAttribute("key", s);
-                property.addAttribute("value", configuration.getProperties().get(s));
-            }
+            rootElem.add(new ProjectConfigGenerator(configuration).generateXmlElement());
         }
-        return projectDom;
+        return rootElem;
     }
 }
diff --git a/src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java b/src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java
new file mode 100644
index 0000000..7d3faf3
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/XmlDocumentGenerator.java
@@ -0,0 +1,29 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.Document;
+import org.dom4j.Element;
+import org.dom4j.Node;
+
+/**
+ * XmlDocumentGenerator is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public interface XmlDocumentGenerator {
+
+    /**
+     * Generate the XML {@link org.dom4j.Node}
+     *
+     * @return any object holding the converted value
+     */
+    Element generateXmlElement();
+    /**
+     * Generate the XML {@link org.dom4j.Node}
+     *
+     * @param node
+     *
+     * @return any object holding the converted value
+     */
+    Document generateXmlDocument();
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 4ab1fe7..0034aa6 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -101,6 +101,20 @@ public class RundeckClientTest {
         Assert.assertEquals(9,config.getProperties().size());
         Assert.assertEquals("monkey1", config.getProperties().get("project.name"));
     }
+    @Test
+    @Betamax(tape = "set_project_configv11")
+    public void setProjectConfig() throws Exception {
+        HashMap config = new HashMap();
+        config.put("alphabetty", "spaghetti");
+        config.put("blha.blee", "a big amazing thingy so there.");
+        ProjectConfig result = createClient(TEST_TOKEN_6, 11).setProjectConfig("monkey1", config);
+        Assert.assertNotNull(result);
+        Assert.assertNotNull(result.getProperties());
+        Assert.assertEquals(3, result.getProperties().size());
+        Assert.assertEquals("monkey1", result.getProperties().get("project.name"));
+        Assert.assertEquals("spaghetti", result.getProperties().get("alphabetty"));
+        Assert.assertEquals("a big amazing thingy so there.", result.getProperties().get("blha.blee"));
+    }
 
     @Test
     @Betamax(tape = "get_history")
diff --git a/src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java b/src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java
new file mode 100644
index 0000000..aa6d5da
--- /dev/null
+++ b/src/test/java/org/rundeck/api/generator/ProjectConfigGeneratorTest.java
@@ -0,0 +1,41 @@
+package org.rundeck.api.generator;
+
+import junit.framework.Assert;
+import org.dom4j.Document;
+import org.dom4j.Element;
+import org.dom4j.io.XMLWriter;
+import org.junit.Test;
+import org.rundeck.api.domain.ProjectConfig;
+import org.rundeck.api.domain.RundeckProject;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * ProjectConfigGeneratorTest is ...
+ *
+ * @author greg
+ * @since 2014-02-27
+ */
+public class ProjectConfigGeneratorTest {
+    @Test
+    public void generate() throws Exception {
+        ProjectConfig config = new ProjectConfig();
+        config.setProperty("abc", "123");
+        config.setProperty("monkey.bonanza", "pale\ncomparison");
+
+        Document doc = new ProjectConfigGenerator(config).generateXmlDocument();
+        XMLWriter xmlWriter = new XMLWriter(System.out);
+        xmlWriter.write(doc);
+        xmlWriter.flush();
+        Element configElement = doc.getRootElement();
+        Assert.assertEquals("config", configElement.getName());
+        Assert.assertNotNull(configElement.selectSingleNode("property[1]"));
+        Assert.assertEquals("abc", configElement.selectSingleNode("property[1]/@key").getText());
+        Assert.assertEquals("123", configElement.selectSingleNode("property[1]/@value").getText());
+
+        Assert.assertNotNull(configElement.selectSingleNode("property[2]"));
+        Assert.assertEquals("monkey.bonanza", configElement.selectSingleNode("property[2]/@key").getText());
+        Assert.assertEquals("pale\ncomparison", configElement.selectSingleNode("property[2]/@value").getText());
+
+    }
+}
diff --git a/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java b/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
index a9c5b1b..26beb33 100644
--- a/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
+++ b/src/test/java/org/rundeck/api/generator/ProjectGeneratorTest.java
@@ -17,7 +17,7 @@ public class ProjectGeneratorTest {
         RundeckProject project = new RundeckProject();
         project.setName("monkey1");
 
-        Document doc = new ProjectGenerator(project).generate();
+        Document doc = new ProjectGenerator(project).generateXmlDocument();
         Assert.assertEquals("project", doc.getRootElement().getName());
         Assert.assertNotNull(doc.selectSingleNode("/project/name"));
         Assert.assertEquals("monkey1", doc.selectSingleNode("/project/name").getText());
diff --git a/src/test/resources/betamax/tapes/set_project_configv11.yaml b/src/test/resources/betamax/tapes/set_project_configv11.yaml
new file mode 100644
index 0000000..573eeaa
--- /dev/null
+++ b/src/test/resources/betamax/tapes/set_project_configv11.yaml
@@ -0,0 +1,26 @@
+!tape
+name: set_project_configv11
+interactions:
+- recorded: 2014-02-27T21:00:27.197Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/monkey1/config
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=19npj7cd0hpm71nfljn7nlbvh8;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGNvbmZpZz4KICA8cHJvcGVydHkga2V5PSdwcm9qZWN0Lm5hbWUnIHZhbHVlPSdtb25rZXkxJyAvPgogIDxwcm9wZXJ0eSBrZXk9J2FscGhhYmV0dHknIHZhbHVlPSdzcGFnaGV0dGknIC8+CiAgPHByb3BlcnR5IGtleT0nYmxoYS5ibGVlJyB2YWx1ZT0nYSBiaWcgYW1hemluZyB0aGluZ3kgc28gdGhlcmUuJyAvPgo8L2NvbmZpZz4=

From 29459d9ee1195c054fe4f76f682eda1ebd1bece0 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Mar 2014 12:16:10 -0800
Subject: [PATCH 40/89] Support http DELETE with expected 204 no content

---
 src/main/java/org/rundeck/api/ApiCall.java    | 31 ++++++++++++++++---
 .../org/rundeck/api/RundeckApiException.java  | 22 +++++++++++++
 2 files changed, 48 insertions(+), 5 deletions(-)

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index 8d9f857..93993ad 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -325,6 +325,22 @@ class ApiCall {
             RundeckApiLoginException, RundeckApiTokenException {
         return execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath), parser);
     }
+    /**
+     * Execute an HTTP DELETE request to the RunDeck instance, on the given path, and expect a 204 response.
+     *
+     * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
+     * @throws RundeckApiException in case of error when calling the API
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     */
+    public void delete(ApiPathBuilder apiPath) throws RundeckApiException,
+            RundeckApiLoginException, RundeckApiTokenException {
+
+        InputStream response = execute(new HttpDelete(client.getUrl() + client.getApiEndpoint() + apiPath));
+        if(null!=response){
+            throw new RundeckApiException("Unexpected Rundeck response content, expected no content!");
+        }
+    }
 
     /**
      * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call. At the end,
@@ -377,7 +393,8 @@ class ApiCall {
 
             // in case of error, we get a redirect to /api/error
             // that we need to follow manually for POST and DELETE requests (as GET)
-            if (response.getStatusLine().getStatusCode() / 100 == 3) {
+            int statusCode = response.getStatusLine().getStatusCode();
+            if (statusCode / 100 == 3) {
                 String newLocation = response.getFirstHeader("Location").getValue();
                 try {
                     EntityUtils.consume(response.getEntity());
@@ -387,22 +404,26 @@ class ApiCall {
                 request = new HttpGet(newLocation);
                 try {
                     response = httpClient.execute(request);
+                    statusCode = response.getStatusLine().getStatusCode();
                 } catch (IOException e) {
                     throw new RundeckApiException("Failed to execute an HTTP GET on url : " + request.getURI(), e);
                 }
             }
 
             // check the response code (should be 2xx, even in case of error : error message is in the XML result)
-            if (response.getStatusLine().getStatusCode() / 100 != 2) {
-                if (response.getStatusLine().getStatusCode() == 403 && 
+            if (statusCode / 100 != 2) {
+                if (statusCode == 403 &&
                         (client.getToken() != null || client.getSessionID() != null)) {
                     throw new RundeckApiTokenException("Invalid Token or sessionID ! Got HTTP response '" + response.getStatusLine()
                                                        + "' for " + request.getURI());
                 } else {
-                    throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for "
-                                                  + request.getURI());
+                    throw new RundeckApiException.RundeckApiHttpStatusException("Invalid HTTP response '" + response.getStatusLine() + "' for "
+                                                  + request.getURI(), statusCode);
                 }
             }
+            if(statusCode==204){
+                return null;
+            }
             if (response.getEntity() == null) {
                 throw new RundeckApiException("Empty RunDeck response ! HTTP status line is : "
                                               + response.getStatusLine());
diff --git a/src/main/java/org/rundeck/api/RundeckApiException.java b/src/main/java/org/rundeck/api/RundeckApiException.java
index 141e905..2a5b1ca 100644
--- a/src/main/java/org/rundeck/api/RundeckApiException.java
+++ b/src/main/java/org/rundeck/api/RundeckApiException.java
@@ -82,5 +82,27 @@ public class RundeckApiException extends RuntimeException {
             super(message, cause);
         }
     }
+    /**
+     * Error due to unexpected HTTP status
+     */
+    public static class RundeckApiHttpStatusException extends RundeckApiAuthException {
+
+        private static final long serialVersionUID = 1L;
+        private int statusCode;
+
+        public RundeckApiHttpStatusException(String message, int statusCode) {
+            super(message);
+            this.statusCode = statusCode;
+        }
+
+        public RundeckApiHttpStatusException(String message, Throwable cause, int statusCode) {
+            super(message, cause);
+            this.statusCode = statusCode;
+        }
+
+        public int getStatusCode() {
+            return statusCode;
+        }
+    }
 
 }

From f49aa63043b253944bffaf642bfc037c033e335c Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Mar 2014 12:22:10 -0800
Subject: [PATCH 41/89] Support keyed config get/set/delete for apiv11

---
 .../java/org/rundeck/api/RundeckClient.java   | 87 +++++++++++++++++++
 .../rundeck/api/domain/ConfigProperty.java    | 68 +++++++++++++++
 .../ProjectConfigPropertyGenerator.java       | 28 ++++++
 .../parser/ProjectConfigPropertyParser.java   | 41 +++++++++
 .../org/rundeck/api/RundeckClientTest.java    | 29 +++++++
 .../ProjectConfigPropertyParserTest.java      | 48 ++++++++++
 .../tapes/delete_project_config_keyedv11.yaml | 57 ++++++++++++
 .../get_project_config_keyed_dne_v11.yaml     | 22 +++++
 .../tapes/get_project_config_keyedv11.yaml    | 24 +++++
 .../tapes/set_project_config_keyedv11.yaml    | 26 ++++++
 10 files changed, 430 insertions(+)
 create mode 100644 src/main/java/org/rundeck/api/domain/ConfigProperty.java
 create mode 100644 src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java
 create mode 100644 src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java
 create mode 100644 src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java
 create mode 100644 src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml
 create mode 100644 src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml
 create mode 100644 src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml
 create mode 100644 src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 8cf1989..f32f457 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -26,6 +26,7 @@ import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.domain.*;
 import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
 import org.rundeck.api.generator.ProjectConfigGenerator;
+import org.rundeck.api.generator.ProjectConfigPropertyGenerator;
 import org.rundeck.api.generator.ProjectGenerator;
 import org.rundeck.api.parser.*;
 import org.rundeck.api.query.ExecutionQuery;
@@ -380,6 +381,92 @@ public class RundeckClient implements Serializable {
         return new ApiCall(this)
                 .get(new ApiPathBuilder("/project/", projectName, "/config"), new ProjectConfigParser("/config"));
     }
+    /**
+     * Get a single project configuration key
+     *
+     * @param projectName name of the project - mandatory
+     * @param key name of the configuration key
+     *
+     * @return value, or null if the value is not set
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public String getProjectConfig(final String projectName, final String key) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to get the config of a project !");
+        AssertUtil.notBlank(key, "key is mandatory to get the config key value!");
+
+        ConfigProperty configProperty = null;
+        try {
+            configProperty = new ApiCall(this)
+                    .get(new ApiPathBuilder("/project/", projectName, "/config/", key),
+                            new ProjectConfigPropertyParser("/property"));
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            if(404==e.getStatusCode()){
+                return null;
+            }
+            throw e;
+        }
+        return configProperty.getValue();
+    }
+    /**
+     * Set a single project configuration property value
+     *
+     * @param projectName name of the project - mandatory
+     * @param key name of the configuration property
+     * @param value value of the property
+     *
+     * @return new value
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public String setProjectConfig(final String projectName, final String key, final String value) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to set the config of a project !");
+        AssertUtil.notBlank(key, "key is mandatory to set the config key value!");
+        AssertUtil.notBlank(value, "value is mandatory to set the config key value!");
+
+        final ConfigProperty configProperty = new ApiCall(this)
+                .put(new ApiPathBuilder("/project/", projectName, "/config/", key)
+                        .xml(new ProjectConfigPropertyGenerator(new ConfigProperty(key, value))),
+                        new ProjectConfigPropertyParser("/property"));
+
+        return configProperty.getValue();
+    }
+    /**
+     * Set a single project configuration property value
+     *
+     * @param projectName name of the project - mandatory
+     * @param key name of the configuration property
+     * @param value value of the property
+     *
+     * @return new value
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public void deleteProjectConfig(final String projectName, final String key) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to set the config of a project !");
+        AssertUtil.notBlank(key, "key is mandatory to set the config key value!");
+
+        new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName, "/config/",
+                key).accept("application/xml"));
+    }
     /**
      * Return the configuration of a project
      *
diff --git a/src/main/java/org/rundeck/api/domain/ConfigProperty.java b/src/main/java/org/rundeck/api/domain/ConfigProperty.java
new file mode 100644
index 0000000..d15d773
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/ConfigProperty.java
@@ -0,0 +1,68 @@
+package org.rundeck.api.domain;
+
+import java.io.Serializable;
+
+/**
+ * ConfigProperty is a single configuration property key and value.
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ConfigProperty implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    private String key;
+    private String value;
+
+    public ConfigProperty() {
+    }
+
+    public ConfigProperty(String key, String value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ConfigProperty)) return false;
+
+        ConfigProperty that = (ConfigProperty) o;
+
+        if (!key.equals(that.key)) return false;
+        if (!value.equals(that.value)) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = key.hashCode();
+        result = 31 * result + value.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "ConfigProperty{" +
+                "key='" + key + '\'' +
+                ", value='" + value + '\'' +
+                '}';
+    }
+}
diff --git a/src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java
new file mode 100644
index 0000000..f41766b
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectConfigPropertyGenerator.java
@@ -0,0 +1,28 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ConfigProperty;
+
+/**
+ * ProjectConfigPropertyGenerator generates a {@literal } element representing a configuration property.
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ProjectConfigPropertyGenerator extends BaseDocGenerator {
+    private ConfigProperty property;
+
+    public ProjectConfigPropertyGenerator(ConfigProperty property) {
+        this.property = property;
+    }
+
+    @Override
+    public Element generateXmlElement() {
+        Element propElem = DocumentFactory.getInstance().createElement("property");
+        propElem.addAttribute("key", property.getKey());
+        propElem.addAttribute("value", property.getValue());
+
+        return propElem;
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java b/src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java
new file mode 100644
index 0000000..9a57114
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ProjectConfigPropertyParser.java
@@ -0,0 +1,41 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ConfigProperty;
+
+/**
+ * ProjectConfigPropertyParser parses a {@literal } element representing
+ * a configuration property.
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ProjectConfigPropertyParser implements XmlNodeParser {
+    private String xpath;
+
+    public ProjectConfigPropertyParser() {
+    }
+
+    public ProjectConfigPropertyParser(final String xpath) {
+        this.setXpath(xpath);
+    }
+
+    @Override
+    public ConfigProperty parseXmlNode(final Node node) {
+        final Node propnode = getXpath() != null ? node.selectSingleNode(getXpath()) : node;
+        final String key = propnode.valueOf("@key");
+        final String value = propnode.valueOf("@value");
+        final ConfigProperty config = new ConfigProperty();
+        config.setKey(key);
+        config.setValue(value);
+        return config;
+    }
+
+    public String getXpath() {
+        return xpath;
+    }
+
+    public void setXpath(final String xpath) {
+        this.xpath = xpath;
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 0034aa6..ad475fd 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -116,6 +116,35 @@ public class RundeckClientTest {
         Assert.assertEquals("a big amazing thingy so there.", result.getProperties().get("blha.blee"));
     }
 
+    @Test
+    @Betamax(tape = "get_project_config_keyedv11")
+    public void getProjectConfigKeyed() throws Exception {
+        String value = createClient(TEST_TOKEN_6, 11).getProjectConfig("ABC", "project.name");
+        Assert.assertNotNull(value);
+        Assert.assertEquals("ABC", value);
+    }
+    @Test
+    @Betamax(tape = "get_project_config_keyed_dne_v11")
+    public void getProjectConfigKeyedDNE() throws Exception {
+        String value = createClient(TEST_TOKEN_6, 11).getProjectConfig("ABC", "does-not-exist");
+        Assert.assertNull(value);
+    }
+    @Test
+    @Betamax(tape = "set_project_config_keyedv11")
+    public void setProjectConfigKeyed() throws Exception {
+        String value = createClient(TEST_TOKEN_6, 11).setProjectConfig("ABC", "monkey-burrito", "lemon pie");
+        Assert.assertNotNull(value);
+        Assert.assertEquals("lemon pie", value);
+    }
+    @Test
+    @Betamax(tape = "delete_project_config_keyedv11")
+    public void deleteProjectConfigKeyed() throws Exception {
+        RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+        Assert.assertEquals("7up", client1.setProjectConfig("ABC", "monkey-burrito", "7up"));
+        client1.deleteProjectConfig("ABC", "monkey-burrito");
+        String value=client1.getProjectConfig("ABC", "monkey-burrito");
+        Assert.assertNull(value);
+    }
     @Test
     @Betamax(tape = "get_history")
     public void getHistory() throws Exception {
diff --git a/src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java b/src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java
new file mode 100644
index 0000000..0fcac10
--- /dev/null
+++ b/src/test/java/org/rundeck/api/parser/ProjectConfigPropertyParserTest.java
@@ -0,0 +1,48 @@
+package org.rundeck.api.parser;
+
+import junit.framework.Assert;
+import org.dom4j.Document;
+import org.junit.Test;
+import org.junit.runners.JUnit4;
+import org.rundeck.api.domain.ConfigProperty;
+import org.rundeck.api.domain.ProjectConfig;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * Test
+ *
+ * @author greg
+ * @since 2014-03-07
+ */
+public class ProjectConfigPropertyParserTest {
+    @Test
+    public void parseFromProject() throws Exception {
+        InputStream input = getClass().getResourceAsStream("projectv11.xml");
+        Document document = ParserHelper.loadDocument(input);
+
+        ConfigProperty config = new ProjectConfigPropertyParser("project/config/property[1]").parseXmlNode(document);
+        Assert.assertEquals("project.name", config.getKey());
+        Assert.assertEquals("ziggy", config.getValue());
+        /**
+         * 
+         
+
+         */
+    }
+    @Test
+    public void parseProperty() throws Exception {
+        Document document = ParserHelper.loadDocument(new ByteArrayInputStream(("").getBytes()));
+
+        ConfigProperty config = new ProjectConfigPropertyParser("/property").parseXmlNode(document);
+        Assert.assertEquals("project.name", config.getKey());
+        Assert.assertEquals("ABC", config.getValue());
+        /**
+         * 
+         
+
+         */
+    }
+}
diff --git a/src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml b/src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml
new file mode 100644
index 0000000..c107f56
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_project_config_keyedv11.yaml
@@ -0,0 +1,57 @@
+!tape
+name: delete_project_config_keyedv11
+interactions:
+- recorded: 2014-03-07T19:59:51.228Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=bolnwf54stai1bo049hylrsua;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHByb3BlcnR5IGtleT0nbW9ua2V5LWJ1cnJpdG8nIHZhbHVlPSc3dXAnIC8+
+- recorded: 2014-03-07T19:59:51.325Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 204
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+- recorded: 2014-03-07T19:59:51.402Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 404
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '11'
+    body: "\n  \n    property does not exist: monkey-burrito\n  \n"
diff --git a/src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml b/src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml
new file mode 100644
index 0000000..d17090c
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_project_config_keyed_dne_v11.yaml
@@ -0,0 +1,22 @@
+!tape
+name: get_project_config_keyed_dne_v11
+interactions:
+- recorded: 2014-03-07T20:19:47.533Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/project/ABC/config/does-not-exist
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 404
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=2367tnltmmec14cn79ps4fam9;Path=/
+      X-Rundeck-API-Version: '11'
+    body: "\n  \n    property does not exist: does-not-exist\n  \n"
diff --git a/src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml b/src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml
new file mode 100644
index 0000000..c0ba023
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_project_config_keyedv11.yaml
@@ -0,0 +1,24 @@
+!tape
+name: get_project_config_keyedv11
+interactions:
+- recorded: 2014-03-07T19:50:29.035Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/project/ABC/config/project.name
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=jgign9nyxeyp4istq65l86lp;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHByb3BlcnR5IGtleT0ncHJvamVjdC5uYW1lJyB2YWx1ZT0nQUJDJyAvPg==
diff --git a/src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml b/src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml
new file mode 100644
index 0000000..88cfd10
--- /dev/null
+++ b/src/test/resources/betamax/tapes/set_project_config_keyedv11.yaml
@@ -0,0 +1,26 @@
+!tape
+name: set_project_config_keyedv11
+interactions:
+- recorded: 2014-03-07T19:59:51.009Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/ABC/config/monkey-burrito
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=3ssp8chdwsuw16hihk5frgpzy;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHByb3BlcnR5IGtleT0nbW9ua2V5LWJ1cnJpdG8nIHZhbHVlPSdsZW1vbiBwaWUnIC8+

From e8672e025b4f68b3acc0e7f7f64fd73983ef25c0 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Mar 2014 12:35:52 -0800
Subject: [PATCH 42/89] Add deleteProject for apiv11

---
 .../java/org/rundeck/api/RundeckClient.java   | 17 +++++++++
 .../org/rundeck/api/RundeckClientTest.java    | 14 ++++++++
 .../betamax/tapes/delete_projectv11.yaml      | 36 +++++++++++++++++++
 3 files changed, 67 insertions(+)
 create mode 100644 src/test/resources/betamax/tapes/delete_projectv11.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index f32f457..1888684 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -361,6 +361,23 @@ public class RundeckClient implements Serializable {
                                 : "/project"
                         )));
     }
+    /**
+     * Delete a project
+     *
+     * @param projectName name of the project - mandatory
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public void deleteProject(String projectName) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
+        new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName));
+    }
     /**
      * Return the configuration of a project
      *
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index ad475fd..4396061 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -90,6 +90,20 @@ public class RundeckClientTest {
         Assert.assertEquals(null, project.getDescription());
         Assert.assertNotNull(project.getProjectConfig());
 
+    }
+    @Test
+    @Betamax(tape = "delete_projectv11")
+    public void deleteProject() throws Exception {
+        RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+        client1.deleteProject("delete_me");
+        RundeckProject delete_me = null;
+        try {
+            delete_me = client1.getProject("delete_me");
+            Assert.fail();
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            Assert.assertEquals(404,e.getStatusCode());
+        }
+
     }
 
     @Test
diff --git a/src/test/resources/betamax/tapes/delete_projectv11.yaml b/src/test/resources/betamax/tapes/delete_projectv11.yaml
new file mode 100644
index 0000000..4025402
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_projectv11.yaml
@@ -0,0 +1,36 @@
+!tape
+name: delete_projectv11
+interactions:
+- recorded: 2014-03-07T20:34:06.758Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/11/project/delete_me
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 204
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=17vo5m2nghd0e1dcg0hqsuxklu;Path=/
+- recorded: 2014-03-07T20:34:06.903Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/project/delete_me
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 404
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '11'
+    body: "\n  \n    project does not exist: delete_me\n  \n"

From 2ffe87d428049f2d0313302a5eb354afdfbb5d46 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Mar 2014 13:22:02 -0800
Subject: [PATCH 43/89] Update to support writing response content to a stream

---
 src/main/java/org/rundeck/api/ApiCall.java | 123 ++++++++++++++++++---
 1 file changed, 107 insertions(+), 16 deletions(-)

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index 93993ad..f3e7596 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -15,6 +15,7 @@
  */
 package org.rundeck.api;
 
+import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.apache.http.*;
 import org.apache.http.client.HttpClient;
@@ -42,10 +43,7 @@ import org.rundeck.api.parser.XmlNodeParser;
 import org.rundeck.api.util.AssertUtil;
 import org.rundeck.api.util.DocumentContentProducer;
 
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import java.io.*;
 import java.net.ProxySelector;
 import java.security.KeyManagementException;
 import java.security.KeyStoreException;
@@ -356,13 +354,35 @@ class ApiCall {
     private  T execute(HttpRequestBase request, XmlNodeParser parser) throws RundeckApiException,
             RundeckApiLoginException, RundeckApiTokenException {
         // execute the request
-        InputStream response = execute(request);
-
-        // read and parse the response
-        Document xmlDocument = ParserHelper.loadDocument(response);
-        return parser.parseXmlNode(xmlDocument);
+        return new ParserHandler(parser).handle(execute(request, new ResultHandler()));
     }
 
+    /**
+     * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
+     * API call. At the end, the given parser will be used to convert the response to a more useful result object.
+     *
+     * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
+     * @param parser  used to parse the response
+     *
+     * @return the result of the call, as formatted by the parser
+     *
+     * @throws RundeckApiException      in case of error when calling the API
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     */
+    public int get(ApiPathBuilder apiPath, OutputStream outputStream) throws RundeckApiException,
+            RundeckApiLoginException, RundeckApiTokenException, IOException {
+        HttpGet request = new HttpGet(client.getUrl() + client.getApiEndpoint() + apiPath);
+        if (null != apiPath.getAccept()) {
+            request.setHeader("Accept", apiPath.getAccept());
+        }
+        WriteOutHandler handler = new WriteOutHandler(outputStream);
+        int wrote = execute(request, handler);
+        if(handler.thrown!=null){
+            throw handler.thrown;
+        }
+        return wrote;
+    }
     /**
      * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call.
      * 
@@ -374,6 +394,83 @@ class ApiCall {
      */
     private ByteArrayInputStream execute(HttpUriRequest request) throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException {
+        return execute(request, new ResultHandler() );
+    }
+
+    /**
+     * Handles one type into another
+     * @param 
+     * @param 
+     */
+    private static interface Handler{
+        public V handle(T response);
+    }
+
+    /**
+     * Handles parsing inputstream via a parser
+     * @param 
+     */
+    private static class ParserHandler implements Handler {
+        XmlNodeParser parser;
+
+        private ParserHandler(XmlNodeParser parser) {
+            this.parser = parser;
+        }
+
+        @Override
+        public S handle(InputStream response) {
+            // read and parse the response
+            return parser.parseXmlNode(ParserHelper.loadDocument(response));
+        }
+    }
+
+    /**
+     * Handles writing response to an output stream
+     */
+    private static class WriteOutHandler implements Handler {
+        private WriteOutHandler(OutputStream writeOut) {
+            this.writeOut = writeOut;
+        }
+
+        OutputStream writeOut;
+        IOException thrown;
+        @Override
+        public Integer handle(final HttpResponse response) {
+            try {
+                return IOUtils.copy(response.getEntity().getContent(), writeOut);
+            } catch (IOException e) {
+                thrown=e;
+            }
+            return -1;
+        }
+    }
+
+    /**
+     * Handles reading response into a byte array stream
+     */
+    private static class ResultHandler implements Handler {
+        @Override
+        public ByteArrayInputStream handle(final HttpResponse response) {
+            // return a new inputStream, so that we can close all network resources
+            try {
+                return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
+            } catch (IOException e) {
+                throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
+            }
+        }
+    }
+    /**
+     * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call.
+     *
+     * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
+     * @return a new {@link InputStream} instance, not linked with network resources
+     * @throws RundeckApiException in case of error when calling the API
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     */
+    private  T execute(HttpUriRequest request, Handler handler) throws RundeckApiException,
+            RundeckApiLoginException,
+            RundeckApiTokenException {
         HttpClient httpClient = instantiateHttpClient();
         try {
             // we only need to manually login in case of login-based authentication
@@ -428,13 +525,7 @@ class ApiCall {
                 throw new RundeckApiException("Empty RunDeck response ! HTTP status line is : "
                                               + response.getStatusLine());
             }
-
-            // return a new inputStream, so that we can close all network resources
-            try {
-                return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity()));
-            } catch (IOException e) {
-                throw new RundeckApiException("Failed to consume entity and convert the inputStream", e);
-            }
+            return handler.handle(response);
         } finally {
             httpClient.getConnectionManager().shutdown();
         }

From bea99b1c97157595be8c395ba74487451798abc3 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Mar 2014 13:22:59 -0800
Subject: [PATCH 44/89] Add project export for apiv11

---
 .../java/org/rundeck/api/RundeckClient.java   | 49 +++++++++++++++++--
 .../org/rundeck/api/RundeckClientTest.java    | 10 ++++
 .../betamax/tapes/export_projectv11.yaml      | 23 +++++++++
 3 files changed, 77 insertions(+), 5 deletions(-)
 create mode 100644 src/test/resources/betamax/tapes/export_projectv11.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 1888684..bfca253 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -34,11 +34,7 @@ import org.rundeck.api.util.AssertUtil;
 import org.rundeck.api.util.PagedResults;
 import org.rundeck.api.util.ParametersUtil;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.Serializable;
+import java.io.*;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
 
@@ -378,6 +374,49 @@ public class RundeckClient implements Serializable {
         AssertUtil.notBlank(projectName, "projectName is mandatory to create a project !");
         new ApiCall(this).delete(new ApiPathBuilder("/project/", projectName));
     }
+    /**
+     * Convenience method to export the archive of a project to the specified file.
+     *
+     * @param projectName name of the project - mandatory
+     * @param out         file to write to
+     * @return number of bytes written to the stream
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public int exportProject(final String projectName, final File out) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException, IOException {
+        final FileOutputStream fileOutputStream = new FileOutputStream(out);
+        try {
+            return exportProject(projectName, fileOutputStream);
+        }finally {
+            fileOutputStream.close();
+        }
+    }
+    /**
+     * Export the archive of a project to the specified outputstream
+     *
+     * @param projectName name of the project - mandatory
+     * @return number of bytes written to the stream
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public int exportProject(String projectName, OutputStream out) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException, IOException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to export a project archive!");
+        return new ApiCall(this).get(
+                new ApiPathBuilder("/project/", projectName, "/export")
+                        .accept("application/zip"),
+                out);
+    }
     /**
      * Return the configuration of a project
      *
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 4396061..1b93bfa 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -28,6 +28,7 @@ import org.rundeck.api.query.ExecutionQuery;
 import org.rundeck.api.util.PagedResults;
 
 import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.InputStream;
 import java.util.*;
 
@@ -160,6 +161,15 @@ public class RundeckClientTest {
         Assert.assertNull(value);
     }
     @Test
+    @Betamax(tape = "export_projectv11")
+    public void exportProject() throws Exception {
+        RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+        File temp = File.createTempFile("test-archive", ".zip");
+        temp.deleteOnExit();
+        int i = client1.exportProject("DEF1", temp);
+        Assert.assertEquals(8705,i);
+    }
+    @Test
     @Betamax(tape = "get_history")
     public void getHistory() throws Exception {
         final RundeckHistory test = client.getHistory("test");
diff --git a/src/test/resources/betamax/tapes/export_projectv11.yaml b/src/test/resources/betamax/tapes/export_projectv11.yaml
new file mode 100644
index 0000000..595c78d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/export_projectv11.yaml
@@ -0,0 +1,23 @@
+!tape
+name: export_projectv11
+interactions:
+- recorded: 2014-03-07T21:12:45.024Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/project/DEF1/export
+    headers:
+      Accept: application/zip
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+  response:
+    status: 200
+    headers:
+      Content-Disposition: attachment; filename="DEF1-20140307-131244.rdproject.jar"
+      Content-Type: application/zip
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=18wgn0qj6x6ho1vpkwj2logftl;Path=/
+    body: !!binary |-
+      UEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAAUAAQATUVUQS1JTkYvTUFOSUZFU1QuTUb+ygAA803My0xLLS7RDUstKs7Mz7NSMNQz4OUKKs1LSU3O1nUsSs7ILEvVDSjKz0pNLtH1S8xNtVJwcXUzxFTjWlGQX1Si65JYAlRiZGBoomtgrGtgHmJkaGVoZGViEoWpxS2/KDcRp90FBTmZyYklQBmECiM9Az0j3WA/x4BgD/8QXi5eLgBQSwcIeFIRt4MAAADCAAAAUEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAANAAAAcnVuZGVjay1ERUYxLwMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAEgAAAHJ1bmRlY2stREVGMS9qb2JzLwMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAPgAAAHJ1bmRlY2stREVGMS9qb2JzL2pvYi02ZmQxODA4Yy1lYWZiLTQ5YWMtYWJmMi00ZGU3ZWM3NTg3MmYueG1sjVRRb9sgEH7vr+Bhkp9c1qpb0olYmrRV2ksbqdoPIHC2aQl4cFSJpv33YQiO3VbapChwx/fd3XccZk92p5XH5oIQFvfjGndKNp9bebX+uBY18HZX39xyUfNde13fSFiBWH1ar65bRiMwM7TtNLyAbn7c3z0wOpn51MOvAEYAeQYYOqtMt6larj1UxKPjCN1xUxkroW6V81hlWiQKu99zI4sdPXAA0YDoLekVwV55En8jlXz4PS6Xhu/hD6MJV8LQRRxGSz0nW4IXTg2orGkYnVv5fAyZcjKathen2gzCAackg7NPILD59v3uitFilVObAvqZkuwhY8QoPv5XxMW6lAO5qdAFqM7gV0UmvSOFoCWj0oBArHmn9qzXLjzF9qUdCyXMWFStEnxBieggBPi5gEGHThmCxyEKeOxB68eU/H4WYJswSyUxXau64PiyzHy9Bt0xjkmch6zkq+t8RV64DjFJRei/8FuO/YSnPz04TzsHHU2yjpe+fxUkyX+vnniHqfhZ25ZNYPRtq5hUfuAo+omFvQMuhQ0GmzgXc7NAplfRjNfO6NkuCDgIHSRsHQiQaXIz8q2/MBw3zw9Ogmu4F2BkjMbo2XkSsKyWhfDfLz9By8BIaJXG2OopfbabcUa/xJeZB668zdPh1MQFndH0HUpL+jT9BVBLBwgXu2wU9gEAAKQEAABQSwMEFAAICAgAlmlnRAAAAAAAAAAAAAAAAD4AAABydW5kZWNrLURFRjEvam9icy9qb2ItMDk0YjBkY2MtZWQ3NS00MTVmLTg0MDktMzFlNmIyMWI1MDJkLnhtbI1SwVLDIBC99yty4xRJaqJ2hjAetDNe9BsIbJBKIAJx6t9LSNNGvfTEe7tv9+0OSw621coHuskyEvH0RqQELXZVWwjOcxD3dV6VdZc/VMUuvy3hrt2WbV1sBcFROFdoKzV8gaYvr/s3gs90znr4HMFwyD4ABmmVkQ3qmPaAMh8cCyC/G2SsgLxTzgc0l8VCbvueGbHwGIEjcPpufTCsB4ITXdT4l5zgxfbEBXju1BCUNZTgNZvzU0fqexcITnBzGsEEOIazyeDsAXigT8/7kuCFbZYBVmJibFCd4uziEYPW+JFz8H69VM+UzhxwNSgwwTdIOpCP2nKmp11vEhI2ygzK8GXhP83i4P8syThe/Z1JOp0CTreQnnQeP1BLBwg2ZXStGgEAACgCAABQSwMEFAAICAgAlmlnRAAAAAAAAAAAAAAAAD4AAABydW5kZWNrLURFRjEvam9icy9qb2ItMmJlOTY2ZTctNDQwZS00ODk3LThhNTktNmFkMTI2ZGY2MDNlLnhtbL1SPW+DMBDd+RWOOjAhJ5SSIDmWoqaRurRLtyiDwQc4GJtiUyX/vg4fbdR2yNTp7t29p3c6PXLUqRTGUg8h4vpLdZ3gNEwhiWNYBlE0hyBaJctgxR6SIGZ8EcY8j+f3QLAjDgqpCwkfIOnzy+6V4C84bA28d6AyQBVAU2ihirWfM2nAR8a2zEJxXvtKcwhy0RrrDzInzHRdM8Un7CZwgoyW2ljFandADyc2/kH/Q26yVjSWtYVB+NeYktn+cbt52+zvZjgVCqfMlJ4HWalRrVUFZ2Q1KCakORwowaPqX+yFrBDv+A3Gbj2+e8QcBr7Qymmv0bC/fJKaurUhwX3vjdcrC6dvl6bVR8gs3T7tFgRPyJsuuCKTrrs5QT31kj7cx68vfSI/AVBLBwihKksFJgEAAJsCAABQSwMEFAAICAgAlmlnRAAAAAAAAAAAAAAAAD4AAABydW5kZWNrLURFRjEvam9icy9qb2ItZTZmNjFhZTItYWVlMS00NjEwLTlmZTktOGM2ZGYyY2U4ZGJjLnhtbI1Ty07DMBC85yt8QLKElJpUKGolxyeoxIUi8QWuvUndpnawHVSE+Hech9MUOHDyznpmd9YPejC7WjnPEoRoiLs1REoyyMs847BMOUCW3ufZXbouYZ2uRC7LpYCV3AlKAnFQ1Kaq4R1q9vS82VIywWHXwVsLWgA6AjSVUboqcMlrBxg5b7mH6qPA2khIS2Wdx4MsCIU5nbiWEQ8mLZRI8xMUGMTeYNQJXz00Bfa2BXwhBzq3FaqVDty0k6Cbz4696OIvjMisMBkqzzISnLCq8cpo1nVCRve9KJnvRKvkyislceYRzyV/FKCdI+ZNk1HSh8k4v/Zw9lOTxpoDCM8eHjeBGFESDczIVCrXcC/2k9bvLXApTKs9C+I5jJTpelh/O5RcEpECZ1G3El4sCJD9fN2hU/I7HxWW6+PWSrCMOwFahmqUXJKj+Wu7tG3//QZ76niI4XZKVXuwbmo/YLa4pWQMx44/yP0DYEm/9F/iG1BLBwjgWeRUbgEAABwDAABQSwMEFAAICAgAlmlnRAAAAAAAAAAAAAAAAD4AAABydW5kZWNrLURFRjEvam9icy9qb2ItM2Y2ODUzZjgtYjU4OS00ZGE4LWJhNzYtMTYzNmVmODExY2U2LnhtbI1TTU8DIRC976/gYEJispJau9aEclITL2riL6Awu0u7hRVYozH+d4H96Nr0YELCzPDezGMY6M5sG+U8yxCiwY57sJRky7JYr5blOt+u1nf5jeTB4rdFviiWBZTrxUJAQUkA9ozGVA18QMOenh9fKJnc/tTBewdaANoDtJVRutrgkjcOMHLecg/V1wY7D21eKus87mmBKMzhwLUc/V6khRJpfoANBlEbjLSR8BbIG+xtB/gIDnBuK9QoHbB5pKCL74i+ivYPRmSWmPSZZxEJTljVemU0i5WQ0akWJfOTUSo50XpGO3yC6DPVCsXromvka+VQWHNhlCTk+cyUjN0c/LmYlDT1cGrsGbU0VmHetNeUJDMbBGsPn36q21qzA+HZ/cPjgpLRy0ZNMzCVyrXci3ri+toCl8J02rNAnrsjZJoFlkaBkmMgmxommk7CqwUBMl05vnBsz2l8ZFiu9y9WgmXcCdAyZKPkGBzE/5VLu+7fA5+gQxPDe5Wq8WDdVL732dUlJYM5VDwBp2ljWdrS//sFUEsHCNNtNayRAQAAiQMAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAPgAAAHJ1bmRlY2stREVGMS9qb2JzL2pvYi1hYzc4ZGZiYi1hNDljLTQwYWItYWNkOS05NDA4YmU1NjFlNzQueG1sjVPLTsMwELznK3xAsoSUmkKhreT6BEhcKBJf4NqbxDS1g+0gEOLfsZ1HQ9UDUiTvbmZ2x5MNfTO7WjnPMoRoiOMZIiUZF8uVLHa7nC/WIl9c8RAJuc7Xi6vVDm7v5rBcUBKAHaM2ZQ0fULOn58ctJWPavXXw3oIWgPYATWmULje44LUDjJy33EP5tcHaSMgLZZ3HHS0QhTkcuJZD3om0UCDND7DBICqDUSS+emg22NsW8BEc4NyWqFY6YPNIQRffET2L8Q9GZNKYdJ0nFQlOWNV4ZTSLk5DRaRYl0zeDVHKi9Yx2+ATRdapUuDg06Br5SjkUnqkwShLyfGdKBjf7fCom9kDJw9HYM2ppnMK8aW4oSWHWC9YePv04t7HmDYRn9w+Pc0qGLBs0TcBUKtdwL6qR6ysLXArTas8CeZoOkHEXWFoFSo6FbDRM1K2EFwsCZLpy/MLRntP6wLBc77dWgmXcCdAydKPkWOzF/5VL2/bfC5+gvYnB60LVHqwbx3c5m11S0of9xBNw2jaWpSP9f79QSwcI9sg0SZEBAACJAwAAUEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAA+AAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTc1NmM5OWY3LTA0ZmItNDM4Yy04NGY1LTE3MjJkOTY3ZDFjYi54bWytVFtr2zAUfvevUNnAT5mWLm2aoQjCksIe1pSxPZVSFOnYVmNLniSX5N9Xlm9pmo11DAzyOfq+cz8ij3qTS+tohBDx//Xp/6Sg04tLPpsl09HHSbIZTT5d8dHVJLkYjafn52J2ORVjviHYAxtGrtMcniCnX2+u1wT3YnNr4VcFigPaApSpliqdxwnLLcTIOsMcpPt5rLSAUSKNdXFD80Sui4Ip0cleAzvgFHimUSYJDlIHxkfoE+zgwzooR2VepVIhty9hHnNd7hOZQzxAA10lMq18fFKrw5s6DOXM3qfj4xZgnVQBdMtcFqMnllfeKM50AdgI4FvsihLHCP/eSJ1Rz3Smgj+ira4Mh5feflowFqcGUux8RB9sdmSirs/JhAg+LstbShpakFt0nO7bmxPwNntlqU3nHw2a4v8ZtNzI0jGT2sPStmpKzu6+LBc/FnfvzvBGKrxhNouidlaRy6RF/mOogaN64ND778uHm/Vy9XCz+La6v6cEt8ZOR+Wv21VqZT98AV83lOBDqblXrAAKqVaNu3rQ0dg3vFZH3ZQ72A0OS6MfgTu6XF17YCdF/QQNYCKkLZnjWc91mQEmuK6Uo558KHaQ/gWg9ZQTPMgdAnY8rwTcGuAgQqoN8rW+YximtmvfXkOZ5aCEt0bwoGxjfxktqaq/fuUCtK2n3xRfSed3rXffyLSu6WdU15rgVhUN63VAIji8tOEIj+8zUEsHCLN18mQdAgAAhgUAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAPgAAAHJ1bmRlY2stREVGMS9qb2JzL2pvYi0zYWVjN2RmYi05NTAyLTQ4NzYtODc2My0xYWE3ODhjOGZjOWMueG1sjVNNb9QwEL3nV/iAlFN2aAvsFnlzgkpcaC/8ANeeOO4mdvBH1RXivzNONsluURGHyHkz782Mn23+5B47E2JdMMbpP6/0Z1R9I1BuVfNY3X58f1192G0/VfTdVFdCbHc7uWvkreRAxEnROd3hM3b1t+939xwWOGUD/kxoJbID4qCdsXpfNqILWLIQvYioj/vSOoVVY3yI5SQjoXR9L6yaMUVGVog4VEOXtLEsHgfcl9INx8Z0WK7UUW4boxN1MM6eZyiHNvojDUSdFYZo7Eh6ELEt2bPoEhWF1vUIXqE8QOwHePcrd99Y0ePvTSRRyeDtqihbt5SKPuE/2cElL/Gy/Y+APoD2qCF324T2VQkOb+yQw2ufFkvhwlMO89mcMHkhvRnGchzO0ZTPm69RO8uy3Sz7zq6pXQ4Xs+kRX+LScPDuCWWsv3y9u+Iwo2KZfyVzZcIgomwXbWw9CiVdsrEm8TmcKcuVqrPHHFY8M/BFdknhg0eJatzqxPw7Piu8sId7OndfiyDRKqrGYQ2eZr+clqf0389mpBbLfSYnI5300n7Cdfb0M8tecziFivVwz0Qcxqc7LuNr/gNQSwcIg9BA1bgBAADXAwAAUEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAA+AAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOS54bWyNUrFuwjAQ3fkKd/JC5EJoGiRjCZUidSlLN4SEcS7BNNjUdij9+zpxUoLaocv53rt7z2fr6EHvSmkdGyBEfV6fPpMZSx+midhNRZSkcRxN4vwx4vc+TJIkzrKU78R4SolvDIpSFyWcoWQvr8sVJT8wVC18VKAEoHeAU6GlKmY456UFjKwz3EHxNcNKZxDl0liHg8wLhT4euco6XFsJI0+Om8Ii8otm9G79tJi/zdcg9hptK8WPsN1sGCVtQ2dMbpx9uZ2wxRmEfqmV1/ZRqNe+TCoLxq0a3lLScIN2bOXgcr3tZPQBhGOL5+WIkg51VR0ceq8JDKodZziAEUZnXlZgPaFg6PYGfPzU+OYb/hCOr8JcnmGY68oMrbz0hJTcjFB/Tm9+WlX/3oemtd4l0ixTczT79Q1QSwcI8UF0QDUBAABpAgAAUEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAA+AAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWQ0MDIzYTcyLTU0ZTEtNDFiNS1iNGQ0LTRkZDdlNWQ0ZjVlNC54bWyVUj1vwjAQ3fkV7uQlkZvUEYsTCZUidSlLN4REiC/B1LGp7VD67+vEoYDaoZ3s93Hv7NOxvd5KYV0xQYj5e3/6m+AFp/fpQzlN44xCEtNkm8VbymlMOZ9CxmmdAWXEG0OF1I2EI8ji+WWxZOQbBtXCeweqAvQGcGi0UE2O61JawMg6UzpoPnOsNIe4FsY6HMp8YaXbtlT8jPuoyoiDK01jEflBF+xu9Tifvc5WUO002nSqbGGzXheMjIZzMLlJ9vL4whFzCH6hla+9RkHvcwuhLBi3HHibMjKQk/HdysHp0u5g9B4qV8yfFgkjZ3RWdYi4+k5gUJ+Y4wASjI6l7Dx2OwMQuQ89MtZblCcudNtJJwaNe7vp/Jw5SNEKBybHEb4Z3S+90ktyLY4Q1bozkRWn/yQzcvOtfuJXM2Fd9+clG6z9gpJhQ4djWNovUEsHCHXrGxZLAQAAvgIAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAGAAAAHJ1bmRlY2stREVGMS9leGVjdXRpb25zLwMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAKgAAAHJ1bmRlY2stREVGMS9leGVjdXRpb25zL2V4ZWN1dGlvbi0xNDU3LnhtbG1TTW+cMBS851f4UIkT8bLdLZuV15e2kSJVSaXk1JuxH+As2Mg2TaKq/7025mM/KiHBvDfjN/MAAu/Aeye1svQGITJDJMUhyTbbPAl133nVxYOgX0qR7VY7ngIri3Rzx3jKinKdbgTkwPPtLl+XBEdu1Anm4Nkx40DQ9SrbpKvP6Sp/WWf7LNtv818EnzIWzVfddg38X3U3qhZO1FnHXG+pMz0QPILY0b3relfKBjrmahphGvLdGtHoiuALRpSVzGPxqAX8kNYhPM3pOQcQS4cKWSmm/NCrTlSwQod8xcd8BmeKQ+NPpyVrrPe7FEaJqZ6dkaqiqWItoGnEUo88776B39DQh8f7J4JnOK5SBydCWp+K1+NqLoqRqXzJx3dgxqUF9RllKLnaABNc98rRjOBTuJCOAF2lg8c4cMELB9550wv4aYD7hfnsI/e6vmgMU8cnI8BQZjko4U8keClOtvGlbxKD0bDIPfr0R3fhG78N8C/BYzOuAV/tgXRGvwJ39Nv3e594QrHX22BGtNK/meE5lt+0OZaNfkNz8kMyvOcEWWf8p1t9HJIwKS2lsS6ZjXLdtkyJsz0Bp8BrjWqJXC0t8leQ+hzhNqUYePMGzs4hePIz/OR4/svpzQmw9B9QSwcILXMgKd4BAAAOBAAAUEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAApAAAAcnVuZGVjay1ERUYxL2V4ZWN1dGlvbnMvb3V0cHV0LTE0NTcucmRsb2eLK0mtKNGv0C0qzUtJTc7WzclP1y0z0jOI44ozMjA00TUw1jUwDzEytDI0tDK1iKopLkktSEpNz8yrqanOy09JtU3JTM9LzAOL2xqCqeSSCiCrtDi1yDYxJTczr7YGl2EgA4g3LL0oNR23WSQYkZGpUJKRWawARCAtChAtWA22hDgyNS+FUidaQsKOWJMQIefq5xLHBQBQSwcIZFiSoZYAAACnAQAAUEsDBBQACAgIAJZpZ0QAAAAAAAAAAAAAAAAtAAAAcnVuZGVjay1ERUYxL2V4ZWN1dGlvbnMvc3RhdGUtMTQ1Ny5zdGF0ZS5qc29uzZFLq8IwEIX/y1lHaHzgNdvahRs36uZKF6EZpFDTkE5FKP3vN/HBRVDr0u08znznTAc6U9FyWduVgZLT2VzA1oYaqA6mPFhtofYdGiZX8BkKEuJ/acOaKRQ3uzTNsmW2RJ/3Ag35E/l10Am9m8rbLYGiPrqKmAIF+5YEWPsD8frKsr+r5AK6qp5UI2Bat5aDC4HWmXBiWx7jmXEip6NkMkrm27FUUqrZ4hdxQ3t+OfITR8iaIQ1yzSWgGNrF10Ny3XvTHyAMOBnEDAN9+Ehpbq97fOSVmtw99G+gzfs/UEsHCAUn/C3eAAAAlgIAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAFQAAAHJ1bmRlY2stREVGMS9yZXBvcnRzLwMAUEsHCAAAAAACAAAAAAAAAFBLAwQUAAgICACWaWdEAAAAAAAAAAAAAAAAJAAAAHJ1bmRlY2stREVGMS9yZXBvcnRzL3JlcG9ydC0xNDc0LnhtbG1Qz0+DMBS++1f04BVLkcm2ND2oM9GTyebFW2kfgwUooY9ki/F/tw/GnIlJk/Z9P9qvn+yhcz2qG8Zk6ywowWMuJB/PBGKFNSgwpWNlxbCsPAuLaHb7Rdtdqxv4lnwSksWjxsErPxgDYCU/z0Rpg5Vrd6cOfukrjCQGj++9O4BB9bx5CVGuAOKnwK92zCT5ZRzD6r1nfHppwNL1StumasMb00REA97rPag3lzPjmq4GBMvGNN4XQ12fJJ81pLcaYYu6DyqVxCKN4vsoznaJWAuxXmSfkl8rZsfTfPM/ntXZ86sh18FsjmDCR0S6yCS/jBMX0obzQ2HFMl6aCHSRR+lKm0jnRRKlFjIw2WKZJQVZJ/VYgy3deNNAJatC1x5CHX/Ri3Br+qrDucHc0ZceTx8eesLmstUPUEsHCGn21Ec3AQAANQIAAFBLAQIUABQACAgIAJZpZ0R4UhG3gwAAAMIAAAAUAAQAAAAAAAAAAAAAAAAAAABNRVRBLUlORi9NQU5JRkVTVC5NRv7KAABQSwECFAAUAAgICACWaWdEAAAAAAIAAAAAAAAADQAAAAAAAAAAAAAAAADJAAAAcnVuZGVjay1ERUYxL1BLAQIUABQACAgIAJZpZ0QAAAAAAgAAAAAAAAASAAAAAAAAAAAAAAAAAAYBAABydW5kZWNrLURFRjEvam9icy9QSwECFAAUAAgICACWaWdEF7tsFPYBAACkBAAAPgAAAAAAAAAAAAAAAABIAQAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTZmZDE4MDhjLWVhZmItNDlhYy1hYmYyLTRkZTdlYzc1ODcyZi54bWxQSwECFAAUAAgICACWaWdENmV0rRoBAAAoAgAAPgAAAAAAAAAAAAAAAACqAwAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTA5NGIwZGNjLWVkNzUtNDE1Zi04NDA5LTMxZTZiMjFiNTAyZC54bWxQSwECFAAUAAgICACWaWdEoSpLBSYBAACbAgAAPgAAAAAAAAAAAAAAAAAwBQAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTJiZTk2NmU3LTQ0MGUtNDg5Ny04YTU5LTZhZDEyNmRmNjAzZS54bWxQSwECFAAUAAgICACWaWdE4FnkVG4BAAAcAwAAPgAAAAAAAAAAAAAAAADCBgAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWU2ZjYxYWUyLWFlZTEtNDYxMC05ZmU5LThjNmRmMmNlOGRiYy54bWxQSwECFAAUAAgICACWaWdE0201rJEBAACJAwAAPgAAAAAAAAAAAAAAAACcCAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTNmNjg1M2Y4LWI1ODktNGRhOC1iYTc2LTE2MzZlZjgxMWNlNi54bWxQSwECFAAUAAgICACWaWdE9sg0SZEBAACJAwAAPgAAAAAAAAAAAAAAAACZCgAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWFjNzhkZmJiLWE0OWMtNDBhYi1hY2Q5LTk0MDhiZTU2MWU3NC54bWxQSwECFAAUAAgICACWaWdEs3XyZB0CAACGBQAAPgAAAAAAAAAAAAAAAACWDAAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTc1NmM5OWY3LTA0ZmItNDM4Yy04NGY1LTE3MjJkOTY3ZDFjYi54bWxQSwECFAAUAAgICACWaWdEg9BA1bgBAADXAwAAPgAAAAAAAAAAAAAAAAAfDwAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTNhZWM3ZGZiLTk1MDItNDg3Ni04NzYzLTFhYTc4OGM4ZmM5Yy54bWxQSwECFAAUAAgICACWaWdE8UF0QDUBAABpAgAAPgAAAAAAAAAAAAAAAABDEQAAcnVuZGVjay1ERUYxL2pvYnMvam9iLTg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOS54bWxQSwECFAAUAAgICACWaWdEdesbFksBAAC+AgAAPgAAAAAAAAAAAAAAAADkEgAAcnVuZGVjay1ERUYxL2pvYnMvam9iLWQ0MDIzYTcyLTU0ZTEtNDFiNS1iNGQ0LTRkZDdlNWQ0ZjVlNC54bWxQSwECFAAUAAgICACWaWdEAAAAAAIAAAAAAAAAGAAAAAAAAAAAAAAAAACbFAAAcnVuZGVjay1ERUYxL2V4ZWN1dGlvbnMvUEsBAhQAFAAICAgAlmlnRC1zICneAQAADgQAACoAAAAAAAAAAAAAAAAA4xQAAHJ1bmRlY2stREVGMS9leGVjdXRpb25zL2V4ZWN1dGlvbi0xNDU3LnhtbFBLAQIUABQACAgIAJZpZ0RkWJKhlgAAAKcBAAApAAAAAAAAAAAAAAAAABkXAABydW5kZWNrLURFRjEvZXhlY3V0aW9ucy9vdXRwdXQtMTQ1Ny5yZGxvZ1BLAQIUABQACAgIAJZpZ0QFJ/wt3gAAAJYCAAAtAAAAAAAAAAAAAAAAAAYYAABydW5kZWNrLURFRjEvZXhlY3V0aW9ucy9zdGF0ZS0xNDU3LnN0YXRlLmpzb25QSwECFAAUAAgICACWaWdEAAAAAAIAAAAAAAAAFQAAAAAAAAAAAAAAAAA/GQAAcnVuZGVjay1ERUYxL3JlcG9ydHMvUEsBAhQAFAAICAgAlmlnRGn21Ec3AQAANQIAACQAAAAAAAAAAAAAAAAAhBkAAHJ1bmRlY2stREVGMS9yZXBvcnRzL3JlcG9ydC0xNDc0LnhtbFBLBQYAAAAAEwATAN4GAAANGwAAAAA=

From 97fa962311e938ec39008ae84c0401bdbce6f33e Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Sun, 9 Mar 2014 17:17:06 -0700
Subject: [PATCH 45/89] Support request content sent directly via file or
 stream

---
 src/main/java/org/rundeck/api/ApiCall.java    |  9 ++++
 .../java/org/rundeck/api/ApiPathBuilder.java  | 51 +++++++++++++++++++
 2 files changed, 60 insertions(+)

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index f3e7596..e285dee 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -24,7 +24,9 @@ import org.apache.http.client.methods.*;
 import org.apache.http.conn.scheme.Scheme;
 import org.apache.http.conn.ssl.SSLSocketFactory;
 import org.apache.http.conn.ssl.TrustStrategy;
+import org.apache.http.entity.BasicHttpEntity;
 import org.apache.http.entity.EntityTemplate;
+import org.apache.http.entity.FileEntity;
 import org.apache.http.entity.mime.HttpMultipartMode;
 import org.apache.http.entity.mime.MultipartEntity;
 import org.apache.http.entity.mime.content.InputStreamBody;
@@ -298,6 +300,13 @@ class ApiCall {
             } catch (UnsupportedEncodingException e) {
                 throw new RundeckApiException("Unsupported encoding: " + e.getMessage(), e);
             }
+        }else if(apiPath.getContentStream() !=null && apiPath.getContentType()!=null){
+            BasicHttpEntity entity = new BasicHttpEntity();
+            entity.setContent(apiPath.getContentStream());
+            entity.setContentType(apiPath.getContentType());
+            httpPost.setEntity(entity);
+        }else if(apiPath.getContentFile() !=null && apiPath.getContentType()!=null){
+            httpPost.setEntity(new FileEntity(apiPath.getContentFile(), apiPath.getContentType()));
         }else if(apiPath.getXmlDocument()!=null) {
             httpPost.setHeader("Content-Type", "application/xml");
             httpPost.setEntity(new EntityTemplate(new DocumentContentProducer(apiPath.getXmlDocument())));
diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java
index bf27ae9..18a6194 100644
--- a/src/main/java/org/rundeck/api/ApiPathBuilder.java
+++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java
@@ -15,6 +15,7 @@
  */
 package org.rundeck.api;
 
+import java.io.File;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -46,6 +47,9 @@ class ApiPathBuilder {
     private final Map attachments;
     private final List form = new ArrayList();
     private Document xmlDocument;
+    private InputStream contentStream;
+    private File contentFile;
+    private String contentType;
 
     /** Marker for using the right separator between parameters ("?" or "&") */
     private boolean firstParamDone = false;
@@ -59,6 +63,10 @@ class ApiPathBuilder {
     public ApiPathBuilder(String... paths) {
         apiPath = new StringBuilder();
         attachments = new HashMap();
+        paths(paths);
+    }
+
+    public ApiPathBuilder paths(String... paths) {
         if (paths != null) {
             for (String path : paths) {
                 if (StringUtils.isNotBlank(path)) {
@@ -66,6 +74,7 @@ class ApiPathBuilder {
                 }
             }
         }
+        return this;
     }
 
     /**
@@ -269,6 +278,36 @@ class ApiPathBuilder {
         }
         return this;
     }
+    /**
+     * When POSTing a request, use the given {@link InputStream} as the content of the request. This
+     * will only add the stream if it is not null.
+     *
+     * @param contentType MIME content type ofr hte request
+     * @param stream content stream
+     * @return this, for method chaining
+     */
+    public ApiPathBuilder content(final String contentType, final InputStream stream) {
+        if (stream != null && contentType != null) {
+            this.contentStream=stream;
+            this.contentType=contentType;
+        }
+        return this;
+    }
+    /**
+     * When POSTing a request, use the given {@link File} as the content of the request. This
+     * will only add the stream if it is not null.
+     *
+     * @param contentType MIME content type ofr hte request
+     * @param file content from a file
+     * @return this, for method chaining
+     */
+    public ApiPathBuilder content(final String contentType, final File file) {
+        if (file != null && contentType != null) {
+            this.contentFile=file;
+            this.contentType=contentType;
+        }
+        return this;
+    }
     /**
      * When POSTing a request, add the given XMl Document as the content of the request.
      *
@@ -352,6 +391,18 @@ class ApiPathBuilder {
         return xmlDocument;
     }
 
+    public InputStream getContentStream() {
+        return contentStream;
+    }
+
+    public String getContentType() {
+        return contentType;
+    }
+
+    public File getContentFile() {
+        return contentFile;
+    }
+
     /**
      * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder}
      *

From 69d9c91ef822491637eb4c947b553865408d2825 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Sun, 9 Mar 2014 17:17:25 -0700
Subject: [PATCH 46/89] Add support for project archive import api v11

---
 .../java/org/rundeck/api/RundeckClient.java   |  38 ++++++++++
 .../org/rundeck/api/domain/ArchiveImport.java |  38 ++++++++++
 .../api/parser/ArchiveImportParser.java       |  36 +++++++++
 .../org/rundeck/api/RundeckClientTest.java    |  45 ++++++++++-
 .../tapes/import_project_failure_v11.yaml     |  27 +++++++
 .../betamax/tapes/import_project_suv11.yaml   |  71 ++++++++++++++++++
 .../org/rundeck/api/test-archive.zip          | Bin 0 -> 8705 bytes
 7 files changed, 251 insertions(+), 4 deletions(-)
 create mode 100644 src/main/java/org/rundeck/api/domain/ArchiveImport.java
 create mode 100644 src/main/java/org/rundeck/api/parser/ArchiveImportParser.java
 create mode 100644 src/test/resources/betamax/tapes/import_project_failure_v11.yaml
 create mode 100644 src/test/resources/betamax/tapes/import_project_suv11.yaml
 create mode 100644 src/test/resources/org/rundeck/api/test-archive.zip

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index bfca253..666af9b 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -417,6 +417,44 @@ public class RundeckClient implements Serializable {
                         .accept("application/zip"),
                 out);
     }
+
+    /**
+     * Import a archive file to the specified project.
+     *
+     * @param projectName name of the project - mandatory
+     * @param archiveFile zip archive file
+     * @param includeExecutions if true, import executions defined in the archive, otherwise skip them
+     * @param preserveJobUuids if true, do not remove UUIDs from imported jobs, otherwise remove them
+     *
+     * @return Result of the import request, may contain a list of import error messages
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent project with this name)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the projectName is blank (null, empty or whitespace)
+     */
+    public ArchiveImport importArchive(final String projectName, final File archiveFile,
+            final boolean includeExecutions, final boolean preserveJobUuids) throws
+            RundeckApiException, RundeckApiLoginException,
+            RundeckApiTokenException, IllegalArgumentException, IOException {
+
+        AssertUtil.notBlank(projectName, "projectName is mandatory to import a project archive!");
+        AssertUtil.notNull(archiveFile, "archiveFile is mandatory to import a project archive!"); ;
+        return callImportProject(projectName, includeExecutions, preserveJobUuids,
+                new ApiPathBuilder().content("application/zip", archiveFile));
+    }
+
+    private ArchiveImport callImportProject(final String projectName, final boolean includeExecutions, final boolean preserveJobUuids,
+            final ApiPathBuilder param) {
+        param.paths("/project/", projectName, "/import")
+        .param("importExecutions", includeExecutions)
+        .param("jobUuidOption", preserveJobUuids ? "preserve" : "remove");
+        return new ApiCall(this).put(
+                param,
+                new ArchiveImportParser()
+        );
+    }
+
     /**
      * Return the configuration of a project
      *
diff --git a/src/main/java/org/rundeck/api/domain/ArchiveImport.java b/src/main/java/org/rundeck/api/domain/ArchiveImport.java
new file mode 100644
index 0000000..7be8112
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/ArchiveImport.java
@@ -0,0 +1,38 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+
+/**
+ * ArchiveImport describes the result of an {@link org.rundeck.api.RundeckClient#importArchive(String, java.io.File,
+ * boolean, boolean)} request.
+ *
+ * @author greg
+ * @since 2014-03-09
+ */
+public class ArchiveImport {
+    private boolean successful;
+    private List errorMessages;
+
+    public ArchiveImport(final boolean successful, final List errorMessages) {
+        this.successful = successful;
+        this.errorMessages = errorMessages;
+    }
+
+    /**
+     * Return true if successful
+     * @return
+     */
+    public boolean isSuccessful() {
+        return successful;
+    }
+
+
+    /**
+     * Return a list of error messages if unsuccessful
+     * @return
+     */
+    public List getErrorMessages() {
+        return errorMessages;
+    }
+
+}
diff --git a/src/main/java/org/rundeck/api/parser/ArchiveImportParser.java b/src/main/java/org/rundeck/api/parser/ArchiveImportParser.java
new file mode 100644
index 0000000..b4e7bda
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/ArchiveImportParser.java
@@ -0,0 +1,36 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.ArchiveImport;
+
+import java.util.ArrayList;
+
+/**
+ * ArchiveImportParser is ...
+ *
+ * @author greg
+ * @since 2014-03-09
+ */
+public class ArchiveImportParser implements XmlNodeParser {
+    String xpath;
+
+    public ArchiveImportParser() {
+    }
+
+    public ArchiveImportParser(final String xpath) {
+        this.xpath = xpath;
+    }
+
+    @Override
+    public ArchiveImport parseXmlNode(final Node node) {
+        final Node importNode = xpath != null ? node.selectSingleNode(xpath) : node;
+
+        boolean issuccess = "successful".equals(importNode.valueOf("/import/@status"));
+        final ArrayList messages = new ArrayList();
+        for (final Object o : importNode.selectNodes("/import/errors/error")) {
+            messages.add(((Node) o).getText());
+        }
+
+        return new ArchiveImport(issuccess, messages);
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 1b93bfa..f0aa4b6 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -19,6 +19,7 @@ import betamax.Betamax;
 import betamax.MatchRule;
 import betamax.Recorder;
 import betamax.TapeMode;
+import org.apache.commons.io.IOUtils;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Rule;
@@ -27,9 +28,7 @@ import org.rundeck.api.domain.*;
 import org.rundeck.api.query.ExecutionQuery;
 import org.rundeck.api.util.PagedResults;
 
-import java.io.ByteArrayInputStream;
-import java.io.File;
-import java.io.InputStream;
+import java.io.*;
 import java.util.*;
 
 
@@ -167,7 +166,45 @@ public class RundeckClientTest {
         File temp = File.createTempFile("test-archive", ".zip");
         temp.deleteOnExit();
         int i = client1.exportProject("DEF1", temp);
-        Assert.assertEquals(8705,i);
+        Assert.assertEquals(8705, i);
+    }
+    @Test
+    @Betamax(tape = "import_project_suv11",mode = TapeMode.READ_ONLY)
+    public void importProjectSuccess() throws Exception {
+        RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+        InputStream resourceAsStream = getClass().getResourceAsStream("test-archive.zip");
+        File temp = File.createTempFile("test-archive", ".zip");
+        temp.deleteOnExit();
+        IOUtils.copy(resourceAsStream, new FileOutputStream(temp));
+        ArchiveImport def1 = client1.importArchive("DEF2", temp, true, true);
+        Assert.assertTrue(def1.isSuccessful());
+        Assert.assertEquals(0, def1.getErrorMessages().size());
+
+        ArchiveImport def2 = client1.importArchive("DEF2", temp, false, true);
+        Assert.assertTrue(def2.isSuccessful());
+        Assert.assertEquals(0, def2.getErrorMessages().size());
+
+        ArchiveImport def3 = client1.importArchive("DEF2", temp, true, false);
+        Assert.assertTrue(def3.isSuccessful());
+        Assert.assertEquals(0, def3.getErrorMessages().size());
+        temp.delete();
+    }
+    @Test
+    @Betamax(tape = "import_project_failure_v11", mode = TapeMode.READ_ONLY)
+    public void importProjectFailure() throws Exception {
+        RundeckClient client1 = createClient(TEST_TOKEN_6, 11);
+        InputStream resourceAsStream = getClass().getResourceAsStream("test-archive.zip");
+        File temp = File.createTempFile("test-archive", ".zip");
+        temp.deleteOnExit();
+        IOUtils.copy(resourceAsStream, new FileOutputStream(temp));
+        ArchiveImport def1 = client1.importArchive("DEF1", temp, false, true);
+        Assert.assertFalse(def1.isSuccessful());
+        Assert.assertEquals(10, def1.getErrorMessages().size());
+        Assert.assertEquals("Job at index [1] at archive path: " +
+                "rundeck-DEF1/jobs/job-6fd1808c-eafb-49ac-abf2-4de7ec75872f.xml had errors: Validation errors: Cannot" +
+                " create a Job with UUID 6fd1808c-eafb-49ac-abf2-4de7ec75872f: a Job already exists with this UUID. " +
+                "Change the UUID or delete the other Job.", def1.getErrorMessages().get(0));
+
     }
     @Test
     @Betamax(tape = "get_history")
diff --git a/src/test/resources/betamax/tapes/import_project_failure_v11.yaml b/src/test/resources/betamax/tapes/import_project_failure_v11.yaml
new file mode 100644
index 0000000..1ddcae5
--- /dev/null
+++ b/src/test/resources/betamax/tapes/import_project_failure_v11.yaml
@@ -0,0 +1,27 @@
+!tape
+name: import_project_failure_v11
+interactions:
+- recorded: 2014-03-10T00:01:12.170Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/DEF1/import?importExecutions=false&jobUuidOption=preserve
+    headers:
+      Accept: text/xml
+      Content-Length: '8705'
+      Content-Type: application/zip
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+    body: ''
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1q6qo7ev7f5uz11hlfd8e0sa82;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGltcG9ydCBzdGF0dXM9J2ZhaWxlZCc+CiAgPGVycm9ycyBjb3VudD0nMTAnPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItNmZkMTgwOGMtZWFmYi00OWFjLWFiZjItNGRlN2VjNzU4NzJmLnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgNmZkMTgwOGMtZWFmYi00OWFjLWFiZjItNGRlN2VjNzU4NzJmOiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgICA8ZXJyb3I+Sm9iIGF0IGluZGV4IFsxXSBhdCBhcmNoaXZlIHBhdGg6IHJ1bmRlY2stREVGMS9qb2JzL2pvYi0wOTRiMGRjYy1lZDc1LTQxNWYtODQwOS0zMWU2YjIxYjUwMmQueG1sIGhhZCBlcnJvcnM6IFZhbGlkYXRpb24gZXJyb3JzOiBDYW5ub3QgY3JlYXRlIGEgSm9iIHdpdGggVVVJRCAwOTRiMGRjYy1lZDc1LTQxNWYtODQwOS0zMWU2YjIxYjUwMmQ6IGEgSm9iIGFscmVhZHkgZXhpc3RzIHdpdGggdGhpcyBVVUlELiBDaGFuZ2UgdGhlIFVVSUQgb3IgZGVsZXRlIHRoZSBvdGhlciBKb2IuPC9lcnJvcj4KICAgIDxlcnJvcj5Kb2IgYXQgaW5kZXggWzFdIGF0IGFyY2hpdmUgcGF0aDogcnVuZGVjay1ERUYxL2pvYnMvam9iLTJiZTk2NmU3LTQ0MGUtNDg5Ny04YTU5LTZhZDEyNmRmNjAzZS54bWwgaGFkIGVycm9yczogVmFsaWRhdGlvbiBlcnJvcnM6IENhbm5vdCBjcmVhdGUgYSBKb2Igd2l0aCBVVUlEIDJiZTk2NmU3LTQ0MGUtNDg5Ny04YTU5LTZhZDEyNmRmNjAzZTogYSBKb2IgYWxyZWFkeSBleGlzdHMgd2l0aCB0aGlzIFVVSUQuIENoYW5nZSB0aGUgVVVJRCBvciBkZWxldGUgdGhlIG90aGVyIEpvYi48L2Vycm9yPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItZTZmNjFhZTItYWVlMS00NjEwLTlmZTktOGM2ZGYyY2U4ZGJjLnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgZTZmNjFhZTItYWVlMS00NjEwLTlmZTktOGM2ZGYyY2U4ZGJjOiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgICA8ZXJyb3I+Sm9iIGF0IGluZGV4IFsxXSBhdCBhcmNoaXZlIHBhdGg6IHJ1bmRlY2stREVGMS9qb2JzL2pvYi0zZjY4NTNmOC1iNTg5LTRkYTgtYmE3Ni0xNjM2ZWY4MTFjZTYueG1sIGhhZCBlcnJvcnM6IFZhbGlkYXRpb24gZXJyb3JzOiBDYW5ub3QgY3JlYXRlIGEgSm9iIHdpdGggVVVJRCAzZjY4NTNmOC1iNTg5LTRkYTgtYmE3Ni0xNjM2ZWY4MTFjZTY6IGEgSm9iIGFscmVhZHkgZXhpc3RzIHdpdGggdGhpcyBVVUlELiBDaGFuZ2UgdGhlIFVVSUQgb3IgZGVsZXRlIHRoZSBvdGhlciBKb2IuPC9lcnJvcj4KICAgIDxlcnJvcj5Kb2IgYXQgaW5kZXggWzFdIGF0IGFyY2hpdmUgcGF0aDogcnVuZGVjay1ERUYxL2pvYnMvam9iLWFjNzhkZmJiLWE0OWMtNDBhYi1hY2Q5LTk0MDhiZTU2MWU3NC54bWwgaGFkIGVycm9yczogVmFsaWRhdGlvbiBlcnJvcnM6IENhbm5vdCBjcmVhdGUgYSBKb2Igd2l0aCBVVUlEIGFjNzhkZmJiLWE0OWMtNDBhYi1hY2Q5LTk0MDhiZTU2MWU3NDogYSBKb2IgYWxyZWFkeSBleGlzdHMgd2l0aCB0aGlzIFVVSUQuIENoYW5nZSB0aGUgVVVJRCBvciBkZWxldGUgdGhlIG90aGVyIEpvYi48L2Vycm9yPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItNzU2Yzk5ZjctMDRmYi00MzhjLTg0ZjUtMTcyMmQ5NjdkMWNiLnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgNzU2Yzk5ZjctMDRmYi00MzhjLTg0ZjUtMTcyMmQ5NjdkMWNiOiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgICA8ZXJyb3I+Sm9iIGF0IGluZGV4IFsxXSBhdCBhcmNoaXZlIHBhdGg6IHJ1bmRlY2stREVGMS9qb2JzL2pvYi0zYWVjN2RmYi05NTAyLTQ4NzYtODc2My0xYWE3ODhjOGZjOWMueG1sIGhhZCBlcnJvcnM6IFZhbGlkYXRpb24gZXJyb3JzOiBDYW5ub3QgY3JlYXRlIGEgSm9iIHdpdGggVVVJRCAzYWVjN2RmYi05NTAyLTQ4NzYtODc2My0xYWE3ODhjOGZjOWM6IGEgSm9iIGFscmVhZHkgZXhpc3RzIHdpdGggdGhpcyBVVUlELiBDaGFuZ2UgdGhlIFVVSUQgb3IgZGVsZXRlIHRoZSBvdGhlciBKb2IuPC9lcnJvcj4KICAgIDxlcnJvcj5Kb2IgYXQgaW5kZXggWzFdIGF0IGFyY2hpdmUgcGF0aDogcnVuZGVjay1ERUYxL2pvYnMvam9iLTg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOS54bWwgaGFkIGVycm9yczogVmFsaWRhdGlvbiBlcnJvcnM6IENhbm5vdCBjcmVhdGUgYSBKb2Igd2l0aCBVVUlEIDg1OTZjYjljLTY4MzMtNDNmNy1hMGY3LTQ2NjNkZDhhYmMyOTogYSBKb2IgYWxyZWFkeSBleGlzdHMgd2l0aCB0aGlzIFVVSUQuIENoYW5nZSB0aGUgVVVJRCBvciBkZWxldGUgdGhlIG90aGVyIEpvYi48L2Vycm9yPgogICAgPGVycm9yPkpvYiBhdCBpbmRleCBbMV0gYXQgYXJjaGl2ZSBwYXRoOiBydW5kZWNrLURFRjEvam9icy9qb2ItZDQwMjNhNzItNTRlMS00MWI1LWI0ZDQtNGRkN2U1ZDRmNWU0LnhtbCBoYWQgZXJyb3JzOiBWYWxpZGF0aW9uIGVycm9yczogQ2Fubm90IGNyZWF0ZSBhIEpvYiB3aXRoIFVVSUQgZDQwMjNhNzItNTRlMS00MWI1LWI0ZDQtNGRkN2U1ZDRmNWU0OiBhIEpvYiBhbHJlYWR5IGV4aXN0cyB3aXRoIHRoaXMgVVVJRC4gQ2hhbmdlIHRoZSBVVUlEIG9yIGRlbGV0ZSB0aGUgb3RoZXIgSm9iLjwvZXJyb3I+CiAgPC9lcnJvcnM+CjwvaW1wb3J0Pg==
diff --git a/src/test/resources/betamax/tapes/import_project_suv11.yaml b/src/test/resources/betamax/tapes/import_project_suv11.yaml
new file mode 100644
index 0000000..a8530d8
--- /dev/null
+++ b/src/test/resources/betamax/tapes/import_project_suv11.yaml
@@ -0,0 +1,71 @@
+!tape
+name: import_project_suv11
+interactions:
+- recorded: 2014-03-09T23:57:25.471Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/DEF2/import?importExecutions=true&jobUuidOption=preserve
+    headers:
+      Accept: text/xml
+      Content-Length: '8705'
+      Content-Type: application/zip
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+    body: ''
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=10hrj0jebdc621ukdlal6qqyu3;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGltcG9ydCBzdGF0dXM9J3N1Y2Nlc3NmdWwnIC8+
+- recorded: 2014-03-09T23:57:26.403Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/DEF2/import?importExecutions=false&jobUuidOption=preserve
+    headers:
+      Accept: text/xml
+      Content-Length: '8705'
+      Content-Type: application/zip
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+    body: ''
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGltcG9ydCBzdGF0dXM9J3N1Y2Nlc3NmdWwnIC8+
+- recorded: 2014-03-09T23:57:27.155Z
+  request:
+    method: PUT
+    uri: http://rundeck.local:4440/api/11/project/DEF2/import?importExecutions=true&jobUuidOption=remove
+    headers:
+      Accept: text/xml
+      Content-Length: '8705'
+      Content-Type: application/zip
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: Do4d3NUD5DKk21DR4sNK755RcPk618vn
+    body: ''
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGltcG9ydCBzdGF0dXM9J3N1Y2Nlc3NmdWwnIC8+
diff --git a/src/test/resources/org/rundeck/api/test-archive.zip b/src/test/resources/org/rundeck/api/test-archive.zip
new file mode 100644
index 0000000000000000000000000000000000000000..e5a1dab3a2692e8004a0cd9f4ef7ec0d0afae89e
GIT binary patch
literal 8705
zcmbt(2UL^G);7ILk=~1dAP@*iNTGLy2+{GkZUKk3N!=jD?7bii#-A$w7nY2V)^3
zBhuA0Rt0G5X-Vs<>S=3f8W~IJYJKS>B3jq&?mCSGNcGVn0pdN~y^jomgW%~mG~zFi
z+PXc%zLGjU?UKkLI3KS7lR$mn;<7gBzHuKsBnhU2q}^(3QgE5w}sd;d}XwjvX&y!
zM^aLOm9Yu2Ik1_~&Ryk1zcp69DKiEc5-Iy>Ly(i|`wmmW7
z&treaFE63(Z73t#_8gbL4m8u^LgkmoY_h9iy2J2QqR#T_UE}LC6FDbUr5(0uN
zK0uy@xCf6|gN4mu>
z?nts1D5Z)$Z}dnbah`D4qcZ}fyIThzul3m_7^RZzCC4obqMOB(zL++*JdG!PlK+zY
zLC>_Q=J8sgh%Awu{t%U-3wc{bBT@=2Te-ZnBHd=j51DYPQSSuEcAkW&M(S~fwh7V%
zEUPZH#eWWFvrPpeMP#fBpHAUqa^spYt1M6kb3w3$SZi|U&efRnTQ)y9R#Ojo^vTE1!
zQ;rF(uil@ySe1HStG+oYnX>rVi-DQJX7cpD4PG7O9^`JIt`but|Wic3Z-0jz!z~
zuHO|PUj^8DgPY3rmW}|)`S(-EQv+JH8_hvA9@`)2k@QtVos>^IKul{JTKC8RerGMH
zdb}C8eXSFGla!5Hr0M}P#S@8cR<7g?25?hH30d6b`_d)*u+Uzgp1>WR2G4dSA_}L3
zSi1Ljwn9MsJhTG!+?b+}XGT4gkfath_x%jcvQ>`DLY{l>7lB2Tq0Cl;J4tENEnCgx
z0Urk$<7MNgWmDIUPQDLfr_%HE=4v2=UwPCqy-XaCb*6VeZaTs(k=Vq}&Q(8sc6s(_
z*ziuQ_iAvhac_JNkEg5{umN^T?$*swulspc9!yNo&Lf?x*cs&)v(sNCJK)v_>DqGX
zBzo1iGJN=y2`oUdJ`cI@EQsO5#jhj9uUP=uVG&R$76yPofLH(oj(`E+C|Lvmio(c%
zpcs275R4_TP;02@5e8(O*k!#T2NV>Ij}t&7-n}3Dv;tWz7S=!s=Sa0P68Tz3&+j(q
z*FL!TaiTWQUry0$FrTH>kU?;E59yme4LL0sg*KxwfJ6$Ii*(tx@Wlz(3fv;PeB3iMCUMlP#o1qlK@|E$^{cvjxd|k(GD3A{mw4hc^*mkhfcOjPAGj~BRkWcD)so-?cy4Qs^Vsx(s)~m^u=S!!~{mN#gXL4
z@rC!l+BH3CQ?tK?0{z
z9v+`!EaoiICixadP3c)l(@{da;I7S8hncIJFMRdHuor1v9+PTN}?_8-f~8BMla`
z=0tKNkOYIL;+`EYlpF|;Cz0=So{ye7g~X<}g-KFHAkrTv9%VQVh(EQ?%)-$;X*)=5
zqX;42sv^e;Qp#1c79`M*SKQ7@?Y&X^ZdN*`y$HxD-V0(%D=k>-bR70o)7q{TOJ30^
zobQwnIsf)(ugE2qtaPEby0;DbBRk;rTb^4+w_8(4se1YNV3#=znCQCOjm1YirMGSm
zuK+wxoGw(PokhgNF9w=jl8?7!iY0L{sAjU3q`Gy0o5(i$B+t*9xHLDcQyL>rY<|T#
z-ZCy{*QKVy?dt(W(EEU^Gk!W}gG$|{kGCNMjFZr<1(
z&nyq=zdLJSdnjBMY!3(6$-)u%Op3xkp5{B
zDvif+5tb68pCC`ENy$B|!;1D_r1MZlL
z*j2W6?qxU19V&WHcZoqU-qH*AvZ7b6)?M3T(846%*+_OcO08!$6o`dHC*11GXA@b!
zZ6J>53)GUwg}wt1(X=exb(wd0qi^PoiWi>W?T(L`;Q9RIPCR7O(|MV*5@LdytQd(G
z26L0SFk}W7jF~?ooq2uGG#47z*QKQk1}1b?jzx_P6Z+g}|JVI*>2APHwkQ@)e$>AtABBd&G4^(L02Bm)20(x)
z{1Y0303aYhxE)p&DuabV{^?k&Wj~zXBRgd&#$+=+n%WGh4ErF!I6GCAeo}pi)KGZE
z-QKV5Hr+)q;ar38=X#FwmJ(13v`6O8e>~M$}>Hl_`pQYcKwtp`_xgz=rug~{dauQMOg{t
z2jI!SeXGxjO=JKTX6$u$GsbU+tk1W=_%R&vj%!y3!IF^=)M5IBbI_rH?c>CcZ>e-N
z^}PZ+JEb)~b8;8cMh(*r^C=y`tIOWfg*$9B-1OGDBFPQWeg5s}7sM_}&|1yxDRKp8c_{Dca1e
z+Ng#lwIRMD?lyl!s+D1XleUwL`T3f5?Z)LeU6>Jq!SZ5Y#jn
zudCq@ds%=C3*nIudszd~NTpPffxb5F
za!y?(tF^u%;U^r|^A;BD{1B|@I>A?@+*-v0JQ)IcHXg+zWZQP$E;FX&*GNZ6;`0;5
z)7z-K0L|@kO+5GZFBDaT7#zFqdtQBAqc}SBL|=zBsr)oczkh
ze};la-}N(z?YgJ=1^wW1PT=&*{gtlB)J?BV&qq5|PwVr@s$2C=z9|_y>x@;#5!=Ax
zr`7YHm=+Py`~)4*zBOr`%tm&8w@glwE^T0pIU?o(wQn&W1e@Eekfhj~*hHrfmR5B;awD~cB#j7L(YO<3ucd{vIYpH<9I-CO|^HsT}S
zPZ(aju((40FH;Z-U%cVN5CDOf*Lb;y&p!AM7$Ac}!QgN-+#ZeikGRnF7z8rbE|~6w
z#2fiJ(k`=I@DA0nbd>%~G?D@Tc-(LGPZ!4l6;ioN&R$ftv+VOg9h&0In2Jmo(u=ENa&sroklwA702aMfsAE<*N@&Yu|^DU
zQdws!s4P@U64d
z^d8H%np`s3kTZ=&^y{zFfENU7Ia|GWdivsfQ%elwyUHgd_j`)-t?1bwEgrR*?e|>$
zNR2J3$JN%%0(0z=FNt_#8wKvVvb(t|(ui1i^oNU(l`2p8-2Gnz+Kkf
z@Q7=hp1m48t$FfNb1q*<340{$rOMD_e5M-tmzfGKi-4l-@JR{^2ZQm!3LmymK>Qa3
z3I$^@aFiVyg!q=J49nab2_*rI|0lJ1HAgOCG+6K^%^bUU+jgj^6hyFHc|z^^pQz
z2YAR)EeC58Tg7bKcUu&=&x1Q}xxX;pC~36JK;2pDJo50_MtJ;qjQ%A(k6`n&Une8)
zS5AubOB#7Y_BZJW1g)w1sL0|8a{8AQ0tNyEfl)9JKo&x%5b&ikzz%|e;1wkXhLy$O
z6ASj6lt{qWY~KiC{WvcYn>u(jn)$y3XGpue}W)SMkMnw`G9eST$mpsaRM
z2W2IrdUZGG(q8H`dEWM-Izo3YMvZ9zliFg@>2w;Asn+V0PK<5lR*x9G9xl{AyNfNu
z%yUx=H3m@@kZe$RI#%fG-dHj;f8H)(fr-QE#f??1Yk9E!7oo+H6UV
zYMy{p5a`zJwTaG##r}`B8+_uX-I;9ELJmGAzE>ab>DlUM-kBp}in)=ytl%e4%J_sq
z5j1QOFDm6TrY%ZwJ*Z~!a~k4Q9}VU4wxe~N6VkQ)NH4$*#-XR+Sk}C+!eFoUhrzR^
zV3gJNf(UofL1-LGJrZp;DovoxcZr(~iKngZx3qB_ZmD4duxMW&ClB}ktu-h98$W-$
z1js;SVT7~jW~A$p24s1LkJtK9%xmQnfYgMMq^j6sXSH+Z2UE-VrzKytLv`;Ke65AP
zMJtO7HavsP!PbdLR-OlmR+ev22!hmwTLe3xV+z#1Qx}F>!m@01Vn3M3XOKul`*^nn
zbYAmHcPE>FzWzbVCW-8%`yln(>DMxIDla$1$L(c~R-@9wmv|8~A-RQLk5tjMb*qRuZ_$(OI?f2$91&%+qUMCZ*pSAo{SPPTW)vu
z#Y`JLzIB7hDmSbR?%6;~IeB4#cE>4XKVo-TX!~iM;}N4aQK9Gh_6V_=Gr|EU^s3f!
zJ($A!O4}9pHctA_aM#nVN;jav#l$urF8gTJllX6pZ3krrsFjV3*=kkv*C)>)5~nH<
ziPwYGy+5ggo-L4_bNHNRagiqucmj}HCY5~MYoh9O)FVaWyyq_0oue!PfKR5k_IPHX
zxf}uO6g@qH0iVF1=L5cUExw>G-QXweK7hBtiAntKqyKHY?Zj_(jfby~r?1anTgD6H
z>fsPAq5V*-ujD1*Kdtc>9cM-W;C}Gcxocn$5Yi_L9F_$#$bc$9kP1-c)q)dJ+JI9!
zy1m_n`T0FvXS6GIJ5Ki^yGf(RxI**i<7fm5E8vywP#F;zxTK0A2_-;!6((6LCd*rW
zPM9M$HeWw3_FNH>h5&$u5nx7F6%Gfab^X
z-qRnrrKc%&umW3a(9~b*|De!=TR2TAXVm9)ZY<$)+`wzg=fhXH9?;)cFmub^;^jJS
zy2?qzG<(UZDdhwUtOoL;99yN4xlKa1%e?FRlDamva}03#uF}(McSy+h6Uof>N!Ru|
z%j*m0NuCyD7z^2&S0{$iULH>Nw6oxwOFn-rF9!qCc{^5SY*_Gmmi@eXP%z>X^U3xU
zK{~G&B3T@xEhFk)6F~#~0!QGS%|}Qt?woRRFA2iUC!~xkZg^z9*#FiJBp3Y*n8Ytj
z;%{RV>*0m-!g_jm`TTDIO88e!{&I$2`@afWH~ro-VL+C;ebbF;R%C2P6-4m?xbJqy
z+ZGEa{xEW}?3xF_N4X!tA
z8&7IHZ5OVOVP@~YvaXJ50a>R|`NW=F)JZ+HpvjcW1_HjeOBNwNC%S1C2ABvKP!TCq
z8{t#qz0?=&>9SYQ^rT_DFNyCAKVM+u+wvg>!kKs48axTZ9|75KY!Z{O5PiQ6`|*B>
za2@uu{pxZ|ANjKv!YI7+Z$`L_`feTg$Db~jem9hG6ZPFF@UQ&S&~NurhsXbTe?aI@
zf45WrIR1aRl{$>|W0(AAtOC49{=e@^!1~ksiNjbw_J4oI0^*Cdf5!ULF8pDvA3Ksi
zV>MFz3hPh%%ZIUkY=HfYl}7a|tUv8|9>)5y-|{n7GR?2B{
z(|*ojtRG9WpRxEEeuV}8!;<|l){j-j&sge5e}(m@rQcz!AEoinSo8SM@XvexPpg>2
zScEw7-;hNR+utpfc8tr5C-2(ne&kA|H_2JE`H2Y
h1X=Li!hhxBkVw!+lH&sw5fL-~Zxa7R$4&U}{{ihc>^T4c

literal 0
HcmV?d00001


From c12b2f1459cb9521023ac1f4ff1ed7380b5c8729 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 09:55:48 -0700
Subject: [PATCH 47/89] API 11 status

---
 src/site/confluence/status.confluence | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 3a3d96b..edf1e5f 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -93,3 +93,17 @@ h2. RunDeck API version 10
 * Execution Output - Retrieve log output for a particular node or step - OK
 * Execution Info - added successfulNodes and failedNodes detail. - OK
 * Deprecation: Remove methods deprecated until version 10. - OK
+
+
+h2. RunDeck API version 11
+
+[Documentation of the RunDeck API version 11|http://rundeck.org/2.1.0/api/index.html]
+
+* Project creation - OK
+* Get Project configuration - OK
+* Set Project configuration - OK
+* Get/Set Project configuration keys - OK
+* Delete project - OK
+* Export project archive - OK
+* Import project archive - OK
+* ...

From b5f4ec8ccd42eb371760c61d13d008e666952bdc Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 09:57:20 -0700
Subject: [PATCH 48/89] Add todos for API v11

---
 src/site/confluence/status.confluence | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index edf1e5f..9bc64f9 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -106,4 +106,10 @@ h2. RunDeck API version 11
 * Delete project - OK
 * Export project archive - OK
 * Import project archive - OK
-* ...
+* SSH Key upload - *TODO*
+* SSH Key delete - *TODO*
+* SSH Key list - *TODO*
+* SSH Key get - *TODO*
+* API Token create - *TODO*
+* API Token list - *TODO*
+* API Token delete - *TODO*

From 0cb3da88ec2b38d6cbfd06adf64a22225be988aa Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 11:40:24 -0700
Subject: [PATCH 49/89] Allow empty content in request

---
 src/main/java/org/rundeck/api/ApiCall.java        |  2 ++
 src/main/java/org/rundeck/api/ApiPathBuilder.java | 13 +++++++++++++
 2 files changed, 15 insertions(+)

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index e285dee..07e09f9 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -310,6 +310,8 @@ class ApiCall {
         }else if(apiPath.getXmlDocument()!=null) {
             httpPost.setHeader("Content-Type", "application/xml");
             httpPost.setEntity(new EntityTemplate(new DocumentContentProducer(apiPath.getXmlDocument())));
+        }else if(apiPath.isEmptyContent()){
+            //empty content
         }else {
             throw new IllegalArgumentException("No Form or Multipart entity for POST content-body");
         }
diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java
index 18a6194..05250ec 100644
--- a/src/main/java/org/rundeck/api/ApiPathBuilder.java
+++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java
@@ -50,6 +50,7 @@ class ApiPathBuilder {
     private InputStream contentStream;
     private File contentFile;
     private String contentType;
+    private boolean emptyContent = false;
 
     /** Marker for using the right separator between parameters ("?" or "&") */
     private boolean firstParamDone = false;
@@ -308,6 +309,15 @@ class ApiPathBuilder {
         }
         return this;
     }
+    /**
+     * When POSTing a request, send an empty request.
+     *
+     * @return this, for method chaining
+     */
+    public ApiPathBuilder emptyContent() {
+        this.emptyContent=true;
+        return this;
+    }
     /**
      * When POSTing a request, add the given XMl Document as the content of the request.
      *
@@ -403,6 +413,9 @@ class ApiPathBuilder {
         return contentFile;
     }
 
+    public boolean isEmptyContent() {
+        return emptyContent;
+    }
     /**
      * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder}
      *

From 0f8e3387192c23942695f9c67d9e92771d153349 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 11:41:09 -0700
Subject: [PATCH 50/89] Support API token endpoint (v11)

---
 .../java/org/rundeck/api/RundeckClient.java   | 64 ++++++++++++++
 .../org/rundeck/api/domain/RundeckToken.java  | 36 ++++++++
 .../api/parser/RundeckTokenParser.java        | 33 ++++++++
 .../org/rundeck/api/RundeckClientTest.java    | 84 +++++++++++++++++++
 .../betamax/tapes/api_token_delete.yaml       | 36 ++++++++
 .../betamax/tapes/api_token_generate.yaml     | 25 ++++++
 .../betamax/tapes/api_token_get.yaml          | 24 ++++++
 .../betamax/tapes/api_tokens_list_all.yaml    | 24 ++++++
 .../betamax/tapes/api_tokens_list_user.yaml   | 24 ++++++
 9 files changed, 350 insertions(+)
 create mode 100644 src/main/java/org/rundeck/api/domain/RundeckToken.java
 create mode 100644 src/main/java/org/rundeck/api/parser/RundeckTokenParser.java
 create mode 100644 src/test/resources/betamax/tapes/api_token_delete.yaml
 create mode 100644 src/test/resources/betamax/tapes/api_token_generate.yaml
 create mode 100644 src/test/resources/betamax/tapes/api_token_get.yaml
 create mode 100644 src/test/resources/betamax/tapes/api_tokens_list_all.yaml
 create mode 100644 src/test/resources/betamax/tapes/api_tokens_list_user.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 666af9b..e56bda1 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -2310,6 +2310,70 @@ public class RundeckClient implements Serializable {
         return new ApiCall(this).get(new ApiPathBuilder("/system/info"), new SystemInfoParser(rootXpath()+"/system"));
     }
 
+
+    /*
+     * API token
+     */
+
+    /**
+     * List API tokens for a user.
+     * @param user username
+     * @return list of tokens
+     * @throws RundeckApiException
+     */
+    public List listApiTokens(final String user) throws RundeckApiException {
+        AssertUtil.notNull(user, "user is mandatory to list API tokens for a user.");
+        return new ApiCall(this).
+                get(new ApiPathBuilder("/tokens/", user),
+                        new ListParser(new RundeckTokenParser(), "/tokens/token"));
+    }
+
+    /**
+     * List all API tokens
+     * @return list of tokens
+     * @throws RundeckApiException
+     */
+    public List listApiTokens() throws RundeckApiException {
+        return new ApiCall(this).
+                get(new ApiPathBuilder("/tokens"),
+                        new ListParser(new RundeckTokenParser(), "/tokens/token"));
+    }
+
+    /**
+     * Generate an API token for a user.
+     * @param user
+     * @return
+     * @throws RundeckApiException
+     */
+    public String generateApiToken(final String user) throws RundeckApiException{
+        AssertUtil.notNull(user, "user is mandatory to generate an API token for a user.");
+        RundeckToken result = new ApiCall(this).
+                post(new ApiPathBuilder("/tokens/", user).emptyContent(),
+                        new RundeckTokenParser("/token"));
+        return result.getToken();
+    }
+    /**
+     * Delete an existing token
+     * @param token
+     * @return
+     * @throws RundeckApiException
+     */
+    public boolean deleteApiToken(final String token) throws RundeckApiException{
+        AssertUtil.notNull(token, "token is mandatory to delete an API token.");
+        new ApiCall(this).delete(new ApiPathBuilder("/token/", token));
+        return true;
+    }
+    /**
+     * Return user info for an existing token
+     * @param token
+     * @return token info
+     * @throws RundeckApiException
+     */
+    public RundeckToken getApiToken(final String token) throws RundeckApiException{
+        AssertUtil.notNull(token, "token is mandatory to get an API token.");
+        return new ApiCall(this).get(new ApiPathBuilder("/token/", token), new RundeckTokenParser("/token"));
+    }
+
     /**
      * @return the URL of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc)
      */
diff --git a/src/main/java/org/rundeck/api/domain/RundeckToken.java b/src/main/java/org/rundeck/api/domain/RundeckToken.java
new file mode 100644
index 0000000..613fc82
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/RundeckToken.java
@@ -0,0 +1,36 @@
+package org.rundeck.api.domain;
+
+/**
+ * RundeckToken is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class RundeckToken {
+    private String user;
+    private String token;
+
+    public RundeckToken() {
+    }
+
+    public RundeckToken(String user, String token) {
+        this.setUser(user);
+        this.setToken(token);
+    }
+
+    public String getUser() {
+        return user;
+    }
+
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/RundeckTokenParser.java b/src/main/java/org/rundeck/api/parser/RundeckTokenParser.java
new file mode 100644
index 0000000..b48543d
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/RundeckTokenParser.java
@@ -0,0 +1,33 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.RundeckToken;
+
+/**
+ * RundeckTokenParser is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class RundeckTokenParser implements XmlNodeParser {
+    String xpath;
+
+    public RundeckTokenParser() {
+    }
+
+    public RundeckTokenParser(String xpath) {
+        this.xpath = xpath;
+    }
+
+    @Override
+    public RundeckToken parseXmlNode(Node node) {
+        Node targetNode = xpath != null ? node.selectSingleNode(xpath) : node;
+        RundeckToken rundeckToken = new RundeckToken();
+        String token = targetNode.valueOf("@id");
+        String user = targetNode.valueOf("@user");
+        rundeckToken.setToken(token);
+        rundeckToken.setUser(user);
+
+        return rundeckToken;
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index f0aa4b6..053ff51 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -55,6 +55,7 @@ public class RundeckClientTest {
     public static final String TEST_TOKEN_4 = "sN5RRSNvu15DnV6EcNDdc2CkdPcv3s32";
     public static final String TEST_TOKEN_5 = "C3O6d5O98Kr6Dpv71sdE4ERdCuU12P6d";
     public static final String TEST_TOKEN_6 = "Do4d3NUD5DKk21DR4sNK755RcPk618vn";
+    public static final String TEST_TOKEN_7 = "8Dp9op111ER6opsDRkddvE86K9sE499s";
 
     @Rule
     public Recorder recorder = new Recorder();
@@ -1197,6 +1198,89 @@ public class RundeckClientTest {
         Assert.assertEquals(RundeckWFExecState.SUCCEEDED,output.getExecutionState());
     }
 
+    /**
+     * generate api token
+     */
+    @Test
+    @Betamax(tape = "api_token_generate", mode = TapeMode.READ_ONLY)
+    public void generateApiToken() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        String token = client.generateApiToken("bob");
+
+        Assert.assertNotNull(token);
+        Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", token);
+    }
+    /**
+     * get api token
+     */
+    @Test
+    @Betamax(tape = "api_token_get", mode = TapeMode.READ_ONLY)
+    public void getApiToken() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        RundeckToken token = client.getApiToken("MiquQjELTrEaugpmdgAKs1W3a7xonAwU");
+
+        Assert.assertNotNull(token);
+        Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", token.getToken());
+        Assert.assertEquals("bob", token.getUser());
+    }
+    /**
+     * list api tokens for user
+     */
+    @Test
+    @Betamax(tape = "api_tokens_list_user", mode = TapeMode.READ_ONLY)
+    public void listApiTokens_user() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        List tokens = client.listApiTokens("bob");
+
+        Assert.assertNotNull(tokens);
+        Assert.assertEquals(3, tokens.size());
+        Assert.assertEquals("hINp5eGzvYA9UePbUChaKHd5NiRkwWbx", tokens.get(0).getToken());
+        Assert.assertEquals("bob", tokens.get(0).getUser());
+        Assert.assertEquals("NaNnwVzAHAG83qOS7Wtwh6mjcXViyWUV", tokens.get(1).getToken());
+        Assert.assertEquals("bob", tokens.get(1).getUser());
+        Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", tokens.get(2).getToken());
+        Assert.assertEquals("bob", tokens.get(2).getUser());
+    }
+    /**
+     * list api tokens all
+     */
+    @Test
+    @Betamax(tape = "api_tokens_list_all"/*, mode = TapeMode.READ_ONLY*/)
+    public void listApiTokens() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        List tokens = client.listApiTokens();
+
+        Assert.assertNotNull(tokens);
+        Assert.assertEquals(4, tokens.size());
+        Assert.assertEquals("8Dp9op111ER6opsDRkddvE86K9sE499s", tokens.get(0).getToken());
+        Assert.assertEquals("admin", tokens.get(0).getUser());
+        Assert.assertEquals("hINp5eGzvYA9UePbUChaKHd5NiRkwWbx", tokens.get(1).getToken());
+        Assert.assertEquals("bob", tokens.get(1).getUser());
+        Assert.assertEquals("NaNnwVzAHAG83qOS7Wtwh6mjcXViyWUV", tokens.get(2).getToken());
+        Assert.assertEquals("bob", tokens.get(2).getUser());
+        Assert.assertEquals("MiquQjELTrEaugpmdgAKs1W3a7xonAwU", tokens.get(3).getToken());
+        Assert.assertEquals("bob", tokens.get(3).getUser());
+    }
+
+    /**
+     * get api token
+     */
+    @Test
+    @Betamax(tape = "api_token_delete"/*, mode = TapeMode.READ_ONLY*/)
+    public void deleteApiToken() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+        client.deleteApiToken("MiquQjELTrEaugpmdgAKs1W3a7xonAwU");
+
+        //get should now return 404
+        try {
+            client.getApiToken("MiquQjELTrEaugpmdgAKs1W3a7xonAwU");
+            Assert.fail("expected failure");
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            Assert.assertEquals(404, e.getStatusCode());
+        }
+    }
+
     @Before
     public void setUp() throws Exception {
         // not that you can put whatever here, because we don't actually connect to the RunDeck instance
diff --git a/src/test/resources/betamax/tapes/api_token_delete.yaml b/src/test/resources/betamax/tapes/api_token_delete.yaml
new file mode 100644
index 0000000..92fc6c0
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_token_delete.yaml
@@ -0,0 +1,36 @@
+!tape
+name: api_token_delete
+interactions:
+- recorded: 2014-04-04T18:38:18.432Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/11/token/MiquQjELTrEaugpmdgAKs1W3a7xonAwU
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 204
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=j0fidhqqsmlt1qmvaawr52a42;Path=/
+- recorded: 2014-04-04T18:38:18.523Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/token/MiquQjELTrEaugpmdgAKs1W3a7xonAwU
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 404
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '11'
+    body: "\n  \n    Token does not exist: MiquQjELTrEaugpmdgAKs1W3a7xonAwU\n  \n"
diff --git a/src/test/resources/betamax/tapes/api_token_generate.yaml b/src/test/resources/betamax/tapes/api_token_generate.yaml
new file mode 100644
index 0000000..1a5f72d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_token_generate.yaml
@@ -0,0 +1,25 @@
+!tape
+name: api_token_generate
+interactions:
+- recorded: 2014-04-04T18:21:07.759Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/11/tokens/bob
+    headers:
+      Accept: text/xml
+      Content-Length: '0'
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 201
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1gt9t2gch2zff1a0werz1us5wk;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHRva2VuIGlkPSdNaXF1UWpFTFRyRWF1Z3BtZGdBS3MxVzNhN3hvbkF3VScgdXNlcj0nYm9iJyAvPg==
diff --git a/src/test/resources/betamax/tapes/api_token_get.yaml b/src/test/resources/betamax/tapes/api_token_get.yaml
new file mode 100644
index 0000000..8377d6f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_token_get.yaml
@@ -0,0 +1,24 @@
+!tape
+name: api_token_get
+interactions:
+- recorded: 2014-04-04T18:23:05.986Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/token/MiquQjELTrEaugpmdgAKs1W3a7xonAwU
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1tdpszk6b3v191p0ng2u94rohw;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHRva2VuIGlkPSdNaXF1UWpFTFRyRWF1Z3BtZGdBS3MxVzNhN3hvbkF3VScgdXNlcj0nYm9iJyAvPg==
diff --git a/src/test/resources/betamax/tapes/api_tokens_list_all.yaml b/src/test/resources/betamax/tapes/api_tokens_list_all.yaml
new file mode 100644
index 0000000..f1318ad
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_tokens_list_all.yaml
@@ -0,0 +1,24 @@
+!tape
+name: api_tokens_list_all
+interactions:
+- recorded: 2014-04-04T18:32:37.397Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/tokens
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=ixag173yjktz1c5o9yrbe5z5a;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHRva2VucyBjb3VudD0nNCcgYWxsdXNlcnM9J3RydWUnPgogIDx0b2tlbiBpZD0nOERwOW9wMTExRVI2b3BzRFJrZGR2RTg2SzlzRTQ5OXMnIHVzZXI9J2FkbWluJyAvPgogIDx0b2tlbiBpZD0naElOcDVlR3p2WUE5VWVQYlVDaGFLSGQ1TmlSa3dXYngnIHVzZXI9J2JvYicgLz4KICA8dG9rZW4gaWQ9J05hTm53VnpBSEFHODNxT1M3V3R3aDZtamNYVml5V1VWJyB1c2VyPSdib2InIC8+CiAgPHRva2VuIGlkPSdNaXF1UWpFTFRyRWF1Z3BtZGdBS3MxVzNhN3hvbkF3VScgdXNlcj0nYm9iJyAvPgo8L3Rva2Vucz4=
diff --git a/src/test/resources/betamax/tapes/api_tokens_list_user.yaml b/src/test/resources/betamax/tapes/api_tokens_list_user.yaml
new file mode 100644
index 0000000..15ec26d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/api_tokens_list_user.yaml
@@ -0,0 +1,24 @@
+!tape
+name: api_tokens_list_user
+interactions:
+- recorded: 2014-04-04T18:26:33.394Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/tokens/bob
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=114794elwavo26cx4ugkv7pe7;Path=/
+      X-Rundeck-API-Version: '11'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PHRva2VucyBjb3VudD0nMycgdXNlcj0nYm9iJz4KICA8dG9rZW4gaWQ9J2hJTnA1ZUd6dllBOVVlUGJVQ2hhS0hkNU5pUmt3V2J4JyB1c2VyPSdib2InIC8+CiAgPHRva2VuIGlkPSdOYU5ud1Z6QUhBRzgzcU9TN1d0d2g2bWpjWFZpeVdVVicgdXNlcj0nYm9iJyAvPgogIDx0b2tlbiBpZD0nTWlxdVFqRUxUckVhdWdwbWRnQUtzMVczYTd4b25Bd1UnIHVzZXI9J2JvYicgLz4KPC90b2tlbnM+

From c7153a5613156d253901babd52dc4f891cbb306c Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 11:42:29 -0700
Subject: [PATCH 51/89] API token support added (api v11)

---
 src/site/confluence/status.confluence                | 6 +++---
 src/test/java/org/rundeck/api/RundeckClientTest.java | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 9bc64f9..0dfbb3e 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -110,6 +110,6 @@ h2. RunDeck API version 11
 * SSH Key delete - *TODO*
 * SSH Key list - *TODO*
 * SSH Key get - *TODO*
-* API Token create - *TODO*
-* API Token list - *TODO*
-* API Token delete - *TODO*
+* API Token create - OK
+* API Token list - OK
+* API Token delete - OK
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 053ff51..654334d 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -1245,7 +1245,7 @@ public class RundeckClientTest {
      * list api tokens all
      */
     @Test
-    @Betamax(tape = "api_tokens_list_all"/*, mode = TapeMode.READ_ONLY*/)
+    @Betamax(tape = "api_tokens_list_all", mode = TapeMode.READ_ONLY)
     public void listApiTokens() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
         List tokens = client.listApiTokens();
@@ -1266,7 +1266,7 @@ public class RundeckClientTest {
      * get api token
      */
     @Test
-    @Betamax(tape = "api_token_delete"/*, mode = TapeMode.READ_ONLY*/)
+    @Betamax(tape = "api_token_delete", mode = TapeMode.READ_ONLY)
     public void deleteApiToken() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
 

From 459b498d35a5e6ec52c90f8ea59659ece498d27c Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 13:47:01 -0700
Subject: [PATCH 52/89] Add support for SSH key management (api v11)

---
 src/main/java/org/rundeck/api/ApiCall.java    |  57 ++++-
 .../java/org/rundeck/api/ApiPathBuilder.java  |  11 +
 .../org/rundeck/api/RundeckApiException.java  |  38 ++++
 .../java/org/rundeck/api/RundeckClient.java   | 137 +++++++++++-
 .../api/domain/BaseSSHKeyResource.java        |  61 +++++
 .../api/domain/BaseStorageResource.java       |  86 +++++++
 .../rundeck/api/domain/SSHKeyResource.java    |  23 ++
 .../rundeck/api/domain/StorageResource.java   |  54 +++++
 .../rundeck/api/parser/BaseXpathParser.java   |  29 +++
 .../api/parser/SSHKeyResourceParser.java      |  26 +++
 .../api/parser/StorageResourceParser.java     |  61 +++++
 .../org/rundeck/api/RundeckClientTest.java    | 209 ++++++++++++++++++
 .../betamax/tapes/ssh_key_delete.yaml         |  36 +++
 .../tapes/ssh_key_get_data_private.yaml       |  22 ++
 .../tapes/ssh_key_get_data_public.yaml        |  22 ++
 .../betamax/tapes/ssh_key_get_private.yaml    |  22 ++
 .../betamax/tapes/ssh_key_get_public.yaml     |  22 ++
 .../betamax/tapes/ssh_key_list_directory.yaml |  21 ++
 .../betamax/tapes/ssh_key_list_root.yaml      |  21 ++
 .../betamax/tapes/ssh_key_store_private.yaml  |  25 +++
 .../betamax/tapes/ssh_key_store_public.yaml   |  25 +++
 21 files changed, 1002 insertions(+), 6 deletions(-)
 create mode 100644 src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java
 create mode 100644 src/main/java/org/rundeck/api/domain/BaseStorageResource.java
 create mode 100644 src/main/java/org/rundeck/api/domain/SSHKeyResource.java
 create mode 100644 src/main/java/org/rundeck/api/domain/StorageResource.java
 create mode 100644 src/main/java/org/rundeck/api/parser/BaseXpathParser.java
 create mode 100644 src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
 create mode 100644 src/main/java/org/rundeck/api/parser/StorageResourceParser.java
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_delete.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_get_private.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_get_public.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_list_directory.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_list_root.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_store_private.yaml
 create mode 100644 src/test/resources/betamax/tapes/ssh_key_store_public.yaml

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index 07e09f9..71a9808 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -387,10 +387,14 @@ class ApiCall {
         if (null != apiPath.getAccept()) {
             request.setHeader("Accept", apiPath.getAccept());
         }
-        WriteOutHandler handler = new WriteOutHandler(outputStream);
-        int wrote = execute(request, handler);
-        if(handler.thrown!=null){
-            throw handler.thrown;
+        final WriteOutHandler writeOutHandler = new WriteOutHandler(outputStream);
+        Handler handler = writeOutHandler;
+        if(null!=apiPath.getRequiredContentType()){
+            handler = new RequireContentTypeHandler(apiPath.getRequiredContentType(), handler);
+        }
+        final int wrote = execute(request, handler);
+        if(writeOutHandler.thrown!=null){
+            throw writeOutHandler.thrown;
         }
         return wrote;
     }
@@ -435,6 +439,51 @@ class ApiCall {
         }
     }
 
+    /**
+     * Handles writing response to an output stream
+     */
+    private static class ChainHandler implements Handler {
+        Handler chain;
+        private ChainHandler(Handler chain) {
+            this.chain=chain;
+        }
+        @Override
+        public T handle(final HttpResponse response) {
+            return chain.handle(response);
+        }
+    }
+
+    /**
+     * Handles writing response to an output stream
+     */
+    private static class RequireContentTypeHandler extends ChainHandler {
+        String contentType;
+
+        private RequireContentTypeHandler(final String contentType, final Handler chain) {
+            super(chain);
+            this.contentType = contentType;
+        }
+
+        @Override
+        public T handle(final HttpResponse response) {
+            final Header firstHeader = response.getFirstHeader("Content-Type");
+            final String[] split = firstHeader.getValue().split(";");
+            boolean matched=false;
+            for (int i = 0; i < split.length; i++) {
+                String s = split[i];
+                if (this.contentType.equalsIgnoreCase(s.trim())) {
+                    matched=true;
+                    break;
+                }
+            }
+            if(!matched) {
+                throw new RundeckApiException.RundeckApiHttpContentTypeException(firstHeader.getValue(),
+                        this.contentType);
+            }
+            return super.handle(response);
+        }
+    }
+
     /**
      * Handles writing response to an output stream
      */
diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java
index 05250ec..7ef79ee 100644
--- a/src/main/java/org/rundeck/api/ApiPathBuilder.java
+++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java
@@ -50,6 +50,7 @@ class ApiPathBuilder {
     private InputStream contentStream;
     private File contentFile;
     private String contentType;
+    private String requiredContentType;
     private boolean emptyContent = false;
 
     /** Marker for using the right separator between parameters ("?" or "&") */
@@ -416,6 +417,16 @@ class ApiPathBuilder {
     public boolean isEmptyContent() {
         return emptyContent;
     }
+
+    public ApiPathBuilder requireContentType(String contentType) {
+        this.requiredContentType=contentType;
+        return this;
+    }
+
+    public String getRequiredContentType() {
+        return requiredContentType;
+    }
+
     /**
      * BuildsParameters can add URL or POST parameters to an {@link ApiPathBuilder}
      *
diff --git a/src/main/java/org/rundeck/api/RundeckApiException.java b/src/main/java/org/rundeck/api/RundeckApiException.java
index 2a5b1ca..5a2de8b 100644
--- a/src/main/java/org/rundeck/api/RundeckApiException.java
+++ b/src/main/java/org/rundeck/api/RundeckApiException.java
@@ -105,4 +105,42 @@ public class RundeckApiException extends RuntimeException {
         }
     }
 
+    /**
+     * Error due to unexpected HTTP content-type
+     */
+    public static class RundeckApiHttpContentTypeException extends RundeckApiAuthException {
+
+        private static final long serialVersionUID = 1L;
+        private String contentType;
+        private String requiredContentType;
+
+        public RundeckApiHttpContentTypeException(final String contentType,
+                final String requiredContentType) {
+            super("Unexpected content-type: '" + contentType + "', expected: '" + requiredContentType + "'");
+            this.contentType = contentType;
+            this.requiredContentType = requiredContentType;
+        }
+        public RundeckApiHttpContentTypeException(final String message, final String contentType,
+                final String requiredContentType) {
+            super(message);
+            this.contentType = contentType;
+            this.requiredContentType = requiredContentType;
+        }
+
+        public RundeckApiHttpContentTypeException(final String message, final Throwable cause, final String contentType,
+                final String requiredContentType) {
+            super(message, cause);
+            this.contentType = contentType;
+            this.requiredContentType = requiredContentType;
+        }
+
+        public String getContentType() {
+            return contentType;
+        }
+
+        public String getRequiredContentType() {
+            return requiredContentType;
+        }
+    }
+
 }
diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index e56bda1..b0baa4c 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -19,8 +19,6 @@ import org.apache.commons.io.FileUtils;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.dom4j.Document;
-import org.dom4j.DocumentFactory;
-import org.dom4j.Element;
 import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
 import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.domain.*;
@@ -83,6 +81,8 @@ public class RundeckClient implements Serializable {
 
     private static final long serialVersionUID = 1L;
     public static final String JOBS_IMPORT = "/jobs/import";
+    public static final String STORAGE_ROOT_PATH = "/storage/";
+    public static final String SSH_KEY_PATH = "ssh-key/";
 
     /**
      * Supported version numbers
@@ -2374,6 +2374,139 @@ public class RundeckClient implements Serializable {
         return new ApiCall(this).get(new ApiPathBuilder("/token/", token), new RundeckTokenParser("/token"));
     }
 
+    /**
+     * Store an SSH key file
+     * @param path ssh key storage path, must start with "ssh-key/"
+     * @param keyfile key file
+     * @param privateKey true to store private key, false to store public key
+     * @return the SSH key resource
+     * @throws RundeckApiException
+     */
+    public SSHKeyResource storeSshKey(final String path, final File keyfile, boolean privateKey) throws RundeckApiException{
+        AssertUtil.notNull(path, "path is mandatory to store an SSH key.");
+        AssertUtil.notNull(keyfile, "keyfile is mandatory to store an SSH key.");
+        if (!path.startsWith(SSH_KEY_PATH)) {
+            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+        }
+        return new ApiCall(this).post(
+                new ApiPathBuilder(STORAGE_ROOT_PATH, path).content(
+                        privateKey ? "application/octet-stream" : "application/pgp-keys",
+                        keyfile
+                ),
+                new SSHKeyResourceParser("/resource")
+        );
+    }
+
+    /**
+     * Get metadata for an SSH key file
+     *
+     * @param path ssh key storage path, must start with "ssh-key/"
+     *
+     * @return the ssh key resource
+     *
+     * @throws RundeckApiException if there is an error, or if the path is a directory not a file
+     */
+    public SSHKeyResource getSshKey(final String path) throws RundeckApiException {
+        AssertUtil.notNull(path, "path is mandatory to get an SSH key.");
+        if (!path.startsWith(SSH_KEY_PATH)) {
+            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+        }
+        SSHKeyResource storageResource = new ApiCall(this).get(
+                new ApiPathBuilder(STORAGE_ROOT_PATH, path),
+                new SSHKeyResourceParser("/resource")
+        );
+        if (storageResource.isDirectory()) {
+            throw new RundeckApiException("SSH Key Path is a directory: " + path);
+        }
+        return storageResource;
+    }
+
+    /**
+     * Get content for a public SSH key file
+     * @param path ssh key storage path, must start with "ssh-key/"
+     * @param out outputstream to write data to
+     *
+     * @return length of written data
+     * @throws RundeckApiException
+     */
+    public int getPublicSshKeyContent(final String path, final OutputStream out) throws
+            RundeckApiException, IOException {
+        AssertUtil.notNull(path, "path is mandatory to get an SSH key.");
+        if (!path.startsWith(SSH_KEY_PATH)) {
+            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+        }
+        try {
+            return new ApiCall(this).get(
+                    new ApiPathBuilder(STORAGE_ROOT_PATH, path)
+                            .accept("application/pgp-keys")
+                            .requireContentType("application/pgp-keys"),
+                    out
+            );
+        } catch (RundeckApiException.RundeckApiHttpContentTypeException e) {
+            throw new RundeckApiException("Requested SSH Key path was not a Public key: " + path, e);
+        }
+    }
+
+    /**
+     * Get content for a public SSH key file
+     * @param path ssh key storage path, must start with "ssh-key/"
+     * @param out file to write data to
+     * @return length of written data
+     * @throws RundeckApiException
+     */
+    public int getPublicSshKeyContent(final String path, final File out) throws
+            RundeckApiException, IOException {
+        final FileOutputStream fileOutputStream = new FileOutputStream(out);
+        try {
+            return getPublicSshKeyContent(path, fileOutputStream);
+        }finally {
+            fileOutputStream.close();
+        }
+    }
+
+    /**
+     * List contents of root SSH key directory
+     *
+     * @return list of SSH key resources
+     * @throws RundeckApiException
+     */
+    public List listSshKeyDirectoryRoot() throws RundeckApiException {
+        return listSshKeyDirectory(SSH_KEY_PATH);
+    }
+    /**
+     * List contents of SSH key directory
+     *
+     * @param path ssh key storage path, must start with "ssh-key/"
+     *
+     * @throws RundeckApiException if there is an error, or if the path is a file not a directory
+     */
+    public List listSshKeyDirectory(final String path) throws RundeckApiException {
+        AssertUtil.notNull(path, "path is mandatory to get an SSH key.");
+        if (!path.startsWith(SSH_KEY_PATH)) {
+            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+        }
+        SSHKeyResource storageResource = new ApiCall(this).get(
+                new ApiPathBuilder(STORAGE_ROOT_PATH, path),
+                new SSHKeyResourceParser("/resource")
+        );
+        if(!storageResource.isDirectory()) {
+            throw new RundeckApiException("SSH key path is not a directory path: " + path);
+        }
+        return storageResource.getDirectoryContents();
+    }
+
+    /**
+     * Delete an SSH key file
+     * @param path a path to a SSH key file, must start with "ssh-key/"
+     */
+    public void deleteSshKey(final String path){
+        AssertUtil.notNull(path, "path is mandatory to delete an SSH key.");
+        if (!path.startsWith(SSH_KEY_PATH)) {
+            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+        }
+        new ApiCall(this).delete(new ApiPathBuilder(STORAGE_ROOT_PATH, path));
+    }
+
     /**
      * @return the URL of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc)
      */
diff --git a/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java b/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java
new file mode 100644
index 0000000..b47d878
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java
@@ -0,0 +1,61 @@
+package org.rundeck.api.domain;
+
+import org.rundeck.api.RundeckClient;
+import org.rundeck.api.parser.StorageResourceParser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * BaseSSHKeyResource is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class BaseSSHKeyResource extends BaseStorageResource implements SSHKeyResource {
+    private boolean privateKey;
+
+    public BaseSSHKeyResource() {
+    }
+
+
+    public boolean isPrivateKey() {
+        return privateKey;
+    }
+
+    public void setPrivateKey(boolean privateKey) {
+        this.privateKey = privateKey;
+    }
+
+    ArrayList sshKeyResources = new ArrayList();
+
+    @Override
+    public void setDirectoryContents(List directoryContents) {
+        for (StorageResource directoryContent : directoryContents) {
+            sshKeyResources.add(from(directoryContent));
+        }
+    }
+
+    @Override
+    public List getDirectoryContents() {
+        return sshKeyResources;
+    }
+
+    public static BaseSSHKeyResource from(final StorageResource source) {
+        final BaseSSHKeyResource baseSshKeyResource = new BaseSSHKeyResource();
+        baseSshKeyResource.setDirectory(source.isDirectory());
+        baseSshKeyResource.setPath(source.getPath());
+        baseSshKeyResource.setName(source.getName());
+        baseSshKeyResource.setMetadata(source.getMetadata());
+        baseSshKeyResource.setUrl(source.getUrl());
+        if (!baseSshKeyResource.isDirectory()) {
+            baseSshKeyResource.setPrivateKey(
+                    null != baseSshKeyResource.getMetadata() && "private".equals(baseSshKeyResource.getMetadata().get
+                            ("Rundeck-ssh-key-type"))
+            );
+        } else if (null != source.getDirectoryContents()) {
+            baseSshKeyResource.setDirectoryContents(source.getDirectoryContents());
+        }
+        return baseSshKeyResource;
+    }
+}
diff --git a/src/main/java/org/rundeck/api/domain/BaseStorageResource.java b/src/main/java/org/rundeck/api/domain/BaseStorageResource.java
new file mode 100644
index 0000000..6639ab4
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/BaseStorageResource.java
@@ -0,0 +1,86 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * BaseStorageResource is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class BaseStorageResource implements StorageResource {
+    private String path;
+    private String url;
+    private String name;
+    private Map metadata;
+    private boolean directory;
+    private List directoryContents;
+
+    public BaseStorageResource() {
+    }
+
+    @Override
+    public String getPath() {
+        return path;
+    }
+
+    public void setPath(String path) {
+        this.path = path;
+    }
+
+    @Override
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    @Override
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public Map getMetadata() {
+        return metadata;
+    }
+
+    public void setMetadata(Map metadata) {
+        this.metadata = metadata;
+    }
+
+    @Override
+    public boolean isDirectory() {
+        return directory;
+    }
+
+    public void setDirectory(boolean directory) {
+        this.directory = directory;
+    }
+
+    @Override
+    public List getDirectoryContents() {
+        return directoryContents;
+    }
+
+    public void setDirectoryContents(List directoryContents) {
+        this.directoryContents = directoryContents;
+    }
+
+    @Override
+    public String toString() {
+        return "BaseStorageResource{" +
+                "path='" + path + '\'' +
+                ", url='" + url + '\'' +
+                ", name='" + name + '\'' +
+                ", directory=" + directory +
+                '}';
+    }
+}
diff --git a/src/main/java/org/rundeck/api/domain/SSHKeyResource.java b/src/main/java/org/rundeck/api/domain/SSHKeyResource.java
new file mode 100644
index 0000000..33bbbd5
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/SSHKeyResource.java
@@ -0,0 +1,23 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+
+/**
+ * SSHKeyResource represents a directory or an SSH key file
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public interface SSHKeyResource extends StorageResource {
+    /**
+     * Return true if this is a file and is a private SSH key file.
+     * @return
+     */
+    public boolean isPrivateKey();
+
+    /**
+     * Return the list of SSH Key resources if this is a directory
+     * @return
+     */
+    public List getDirectoryContents();
+}
diff --git a/src/main/java/org/rundeck/api/domain/StorageResource.java b/src/main/java/org/rundeck/api/domain/StorageResource.java
new file mode 100644
index 0000000..be02412
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/StorageResource.java
@@ -0,0 +1,54 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * StorageResource represents a directory or a file
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public interface StorageResource {
+    /**
+     * Return the storage path for this resource
+     *
+     * @return
+     */
+    public String getPath();
+
+    /**
+     * Return the URL for this resource
+     *
+     * @return
+     */
+    public String getUrl();
+
+    /**
+     * Return the file name if this is a file
+     *
+     * @return
+     */
+    public String getName();
+
+    /**
+     * Return the metadata for this file if this is a file
+     *
+     * @return
+     */
+    public Map getMetadata();
+
+    /**
+     * Return true if this is a directory, false if this is a file
+     *
+     * @return
+     */
+    public boolean isDirectory();
+
+    /**
+     * Return the list of directory contents if this is a directory
+     *
+     * @return
+     */
+    public List getDirectoryContents();
+}
diff --git a/src/main/java/org/rundeck/api/parser/BaseXpathParser.java b/src/main/java/org/rundeck/api/parser/BaseXpathParser.java
new file mode 100644
index 0000000..26be541
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/BaseXpathParser.java
@@ -0,0 +1,29 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+
+/**
+ * BaseXpathParser is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public abstract class BaseXpathParser implements XmlNodeParser {
+    private String xpath;
+
+    public BaseXpathParser() {
+    }
+
+    public BaseXpathParser(String xpath) {
+
+        this.xpath = xpath;
+    }
+
+    public abstract T parse(Node node);
+
+    @Override
+    public T parseXmlNode(Node node) {
+        Node selectedNode = xpath != null ? node.selectSingleNode(xpath) : node;
+        return parse(selectedNode);
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
new file mode 100644
index 0000000..5485d31
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
@@ -0,0 +1,26 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.RundeckClient;
+import org.rundeck.api.domain.BaseSSHKeyResource;
+import org.rundeck.api.domain.SSHKeyResource;
+
+/**
+ * SSHKeyResourceParser is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class SSHKeyResourceParser extends BaseXpathParser implements XmlNodeParser {
+    public SSHKeyResourceParser() {
+    }
+
+    public SSHKeyResourceParser(String xpath) {
+        super(xpath);
+    }
+
+    @Override
+    public SSHKeyResource parse(Node node) {
+        return BaseSSHKeyResource.from(new StorageResourceParser().parse(node));
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/StorageResourceParser.java b/src/main/java/org/rundeck/api/parser/StorageResourceParser.java
new file mode 100644
index 0000000..f1c5e6a
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/StorageResourceParser.java
@@ -0,0 +1,61 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Element;
+import org.dom4j.Node;
+import org.rundeck.api.domain.BaseStorageResource;
+import org.rundeck.api.domain.StorageResource;
+
+import java.util.HashMap;
+
+/**
+ * StorageResourceParser is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class StorageResourceParser extends BaseXpathParser {
+    private BaseStorageResource holder;
+    public StorageResourceParser() {
+
+    }
+    public StorageResourceParser(BaseStorageResource holder){
+        this.holder=holder;
+    }
+
+    public StorageResourceParser(String xpath) {
+        super(xpath);
+    }
+
+    @Override
+    public StorageResource parse(Node node) {
+        String path = node.valueOf("@path");
+        String type = node.valueOf("@type");
+        String url = node.valueOf("@url");
+        BaseStorageResource storageResource = null == holder ? new BaseStorageResource() : holder;
+        storageResource.setDirectory("directory".equals(type));
+        storageResource.setPath(path);
+        storageResource.setUrl(url);
+
+        if (storageResource.isDirectory()) {
+            if (node.selectSingleNode("contents") != null) {
+                storageResource.setDirectoryContents(new ListParser(new StorageResourceParser(),
+                        "contents/resource").parseXmlNode(node));
+            }
+        } else {
+            String name = node.valueOf("@name");
+            storageResource.setName(name);
+
+            Node meta = node.selectSingleNode("resource-meta");
+            HashMap metamap = new HashMap();
+            if (null != meta) {
+                Element metaEl = (Element) meta;
+                for (Object o : metaEl.elements()) {
+                    Element sub = (Element) o;
+                    metamap.put(sub.getName(), sub.getText().trim());
+                }
+            }
+            storageResource.setMetadata(metamap);
+        }
+        return storageResource;
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 654334d..f989e69 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -1280,6 +1280,215 @@ public class RundeckClientTest {
             Assert.assertEquals(404, e.getStatusCode());
         }
     }
+    /**
+     * Store ssh key
+     */
+    @Test
+    @Betamax(tape = "ssh_key_store_private", mode = TapeMode.READ_ONLY)
+    public void storeSshKey_private() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        File temp = File.createTempFile("test-key", ".tmp");
+        temp.deleteOnExit();
+        FileOutputStream out = new FileOutputStream(temp);
+        try{
+            out.write("test1".getBytes());
+        }finally {
+            out.close();
+        }
+        SSHKeyResource storageResource = client.storeSshKey("ssh-key/test/example/file1.pem", temp, true);
+        Assert.assertNotNull(storageResource);
+        Assert.assertFalse(storageResource.isDirectory());
+        Assert.assertTrue(storageResource.isPrivateKey());
+        Assert.assertEquals("file1.pem", storageResource.getName());
+        Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem",
+                storageResource.getUrl());
+        Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+        Map metadata = storageResource.getMetadata();
+        Assert.assertNotNull(metadata);
+        Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type"));
+        Assert.assertEquals("private", metadata.get("Rundeck-ssh-key-type"));
+    }
+    /**
+     * Store ssh key
+     */
+    @Test
+    @Betamax(tape = "ssh_key_store_public", mode = TapeMode.READ_ONLY)
+    public void storeSshKey_public() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        File temp = File.createTempFile("test-key", ".tmp");
+        temp.deleteOnExit();
+        FileOutputStream out = new FileOutputStream(temp);
+        try{
+            out.write("test1".getBytes());
+        }finally {
+            out.close();
+        }
+        SSHKeyResource storageResource = client.storeSshKey("ssh-key/test/example/file2.pub", temp, false);
+        Assert.assertNotNull(storageResource);
+        Assert.assertFalse(storageResource.isDirectory());
+        Assert.assertFalse(storageResource.isPrivateKey());
+        Assert.assertEquals("file2.pub", storageResource.getName());
+        Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub",
+                storageResource.getUrl());
+        Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+        Map metadata = storageResource.getMetadata();
+        Assert.assertNotNull(metadata);
+        Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type"));
+        Assert.assertEquals("public", metadata.get("Rundeck-ssh-key-type"));
+    }
+    /**
+     * get ssh key
+     */
+    @Test
+    @Betamax(tape = "ssh_key_get_public", mode = TapeMode.READ_ONLY)
+    public void getSshKey_public() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        SSHKeyResource storageResource = client.getSshKey("ssh-key/test/example/file2.pub");
+        Assert.assertNotNull(storageResource);
+        Assert.assertFalse(storageResource.isDirectory());
+        Assert.assertFalse(storageResource.isPrivateKey());
+        Assert.assertEquals("file2.pub", storageResource.getName());
+        Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub",
+                storageResource.getUrl());
+        Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+        Map metadata = storageResource.getMetadata();
+        Assert.assertNotNull(metadata);
+        Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type"));
+        Assert.assertEquals("public", metadata.get("Rundeck-ssh-key-type"));
+    }
+    /**
+     * get ssh key
+     */
+    @Test
+    @Betamax(tape = "ssh_key_get_private", mode = TapeMode.READ_ONLY)
+    public void getSshKey_private() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        SSHKeyResource storageResource = client.getSshKey("ssh-key/test/example/file1.pem");
+        Assert.assertNotNull(storageResource);
+        Assert.assertFalse(storageResource.isDirectory());
+        Assert.assertTrue(storageResource.isPrivateKey());
+        Assert.assertEquals("file1.pem", storageResource.getName());
+        Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem",
+                storageResource.getUrl());
+        Assert.assertEquals(0, storageResource.getDirectoryContents().size());
+        Map metadata = storageResource.getMetadata();
+        Assert.assertNotNull(metadata);
+        Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type"));
+        Assert.assertEquals("private", metadata.get("Rundeck-ssh-key-type"));
+    }
+    /**
+     * get ssh key data
+     */
+    @Test
+    @Betamax(tape = "ssh_key_get_data_private", mode = TapeMode.READ_ONLY)
+    public void getSshKeyData_private() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        File temp = File.createTempFile("test-key", ".tmp");
+        temp.deleteOnExit();
+        try {
+            int data = client.getPublicSshKeyContent("ssh-key/test/example/file1.pem", temp);
+            Assert.fail("expected failure");
+        } catch (RundeckApiException e) {
+            Assert.assertEquals("Requested SSH Key path was not a Public key: ssh-key/test/example/file1.pem",
+                    e.getMessage());
+        }
+    }
+    /**
+     * get ssh key data
+     */
+    @Test
+    @Betamax(tape = "ssh_key_get_data_public", mode = TapeMode.READ_ONLY)
+    public void getSshKeyData_public() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        File temp = File.createTempFile("test-key", ".tmp");
+        temp.deleteOnExit();
+        int length = client.getPublicSshKeyContent("ssh-key/test/example/file2.pub", temp);
+        Assert.assertEquals(5, length);
+    }
+    /**
+     * list directory
+     */
+    @Test
+    @Betamax(tape = "ssh_key_list_directory", mode = TapeMode.READ_ONLY)
+    public void listSshKeyDirectory() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        List list = client.listSshKeyDirectory("ssh-key/test/example");
+        Assert.assertEquals(2, list.size());
+        SSHKeyResource storageResource1 = list.get(0);
+        SSHKeyResource storageResource2 = list.get(1);
+
+        Assert.assertFalse(storageResource2.isDirectory());
+        Assert.assertTrue(storageResource2.isPrivateKey());
+        Assert.assertEquals("file1.pem", storageResource2.getName());
+        Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource2.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem", storageResource2.getUrl());
+        Assert.assertNotNull(storageResource2.getMetadata());
+
+        Assert.assertEquals("application/octet-stream", storageResource2.getMetadata().get("Rundeck-content-type"));
+        Assert.assertEquals("private", storageResource2.getMetadata().get("Rundeck-ssh-key-type"));
+
+        Assert.assertFalse(storageResource1.isDirectory());
+        Assert.assertFalse(storageResource1.isPrivateKey());
+        Assert.assertEquals("file2.pub", storageResource1.getName());
+        Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource1.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub",
+                storageResource1.getUrl());
+        Assert.assertNotNull(storageResource1.getMetadata());
+        Assert.assertEquals("application/pgp-keys", storageResource1.getMetadata().get("Rundeck-content-type"));
+        Assert.assertEquals("public", storageResource1.getMetadata().get("Rundeck-ssh-key-type"));
+    }
+    /**
+     * list root
+     */
+    @Test
+    @Betamax(tape = "ssh_key_list_root", mode = TapeMode.READ_ONLY)
+    public void listSshKeyDirectoryRoot() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        List list = client.listSshKeyDirectoryRoot();
+        Assert.assertEquals(2, list.size());
+        SSHKeyResource storageResource0 = list.get(0);
+        SSHKeyResource storageResource1 = list.get(1);
+
+        Assert.assertFalse(storageResource0.isDirectory());
+        Assert.assertTrue(storageResource0.isPrivateKey());
+        Assert.assertEquals("test1.pem", storageResource0.getName());
+        Assert.assertEquals("ssh-key/test1.pem", storageResource0.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test1.pem", storageResource0.getUrl());
+        Assert.assertNotNull(storageResource0.getMetadata());
+
+        Assert.assertEquals("application/octet-stream", storageResource0.getMetadata().get("Rundeck-content-type"));
+        Assert.assertEquals("private", storageResource0.getMetadata().get("Rundeck-ssh-key-type"));
+
+        Assert.assertTrue(storageResource1.toString(), storageResource1.isDirectory());
+        Assert.assertEquals(null, storageResource1.getName());
+        Assert.assertEquals("ssh-key/test", storageResource1.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test",
+                storageResource1.getUrl());
+        Assert.assertNull(storageResource1.getMetadata());
+
+
+    }
+
+    /**
+     * delete ssh key
+     */
+    @Test
+    @Betamax(tape = "ssh_key_delete", mode = TapeMode.READ_ONLY)
+    public void deleteSshKey() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        client.deleteSshKey("ssh-key/test/example/file2.pub");
+
+        try {
+            client.getSshKey("ssh-key/test/example/file2.pub");
+            Assert.fail("expected failure");
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            Assert.assertEquals(404,e.getStatusCode());
+        }
+    }
 
     @Before
     public void setUp() throws Exception {
diff --git a/src/test/resources/betamax/tapes/ssh_key_delete.yaml b/src/test/resources/betamax/tapes/ssh_key_delete.yaml
new file mode 100644
index 0000000..fd46eae
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_delete.yaml
@@ -0,0 +1,36 @@
+!tape
+name: ssh_key_delete
+interactions:
+- recorded: 2014-04-04T23:16:02.256Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 204
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=3rsjs6vicpc619b1uy7oshp4y;Path=/
+- recorded: 2014-04-04T23:16:02.372Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 404
+    headers:
+      Content-Type: application/xml;charset=utf-8
+      Server: Jetty(7.6.0.v20120127)
+    body: !!binary |-
+      PGVycm9yPnJlc291cmNlIG5vdCBmb3VuZDogL3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YjwvZXJyb3I+
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml b/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml
new file mode 100644
index 0000000..e5e742b
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml
@@ -0,0 +1,22 @@
+!tape
+name: ssh_key_get_data_private
+interactions:
+- recorded: 2014-04-04T19:50:59.155Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem
+    headers:
+      Accept: application/pgp-keys
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/json;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1gzu37lkjr0fitxhf5fgkgsfu;Path=/
+    body: !!binary |-
+      eyJwYXRoIjoic3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtIiwidHlwZSI6ImZpbGUiLCJuYW1lIjoiZmlsZTEucGVtIiwidXJsIjoiaHR0cDovL2RpZ25hbi5sb2NhbDo0NDQwL2FwaS8xMS9zdG9yYWdlL3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbSIsIm1ldGEiOnsiUnVuZGVjay1jb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJSdW5kZWNrLWNvbnRlbnQtc2l6ZSI6IjUiLCJSdW5kZWNrLWNvbnRlbnQtbWFzayI6ImNvbnRlbnQiLCJSdW5kZWNrLXNzaC1rZXktdHlwZSI6InByaXZhdGUifX0=
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml b/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml
new file mode 100644
index 0000000..1874e71
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml
@@ -0,0 +1,22 @@
+!tape
+name: ssh_key_get_data_public
+interactions:
+- recorded: 2014-04-04T20:20:44.331Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    headers:
+      Accept: application/pgp-keys
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/pgp-keys
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1mrub15qsorpf10cisx24h8h03;Path=/
+    body: !!binary |-
+      dGVzdDE=
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_private.yaml b/src/test/resources/betamax/tapes/ssh_key_get_private.yaml
new file mode 100644
index 0000000..fa0ff2c
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_get_private.yaml
@@ -0,0 +1,22 @@
+!tape
+name: ssh_key_get_private
+interactions:
+- recorded: 2014-04-04T19:47:29.880Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=utf-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=nc5p0he3nw19e4gegidc4bs7;Path=/
+    body: !!binary |-
+      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2stc3NoLWtleS10eXBlPnByaXZhdGU8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_public.yaml b/src/test/resources/betamax/tapes/ssh_key_get_public.yaml
new file mode 100644
index 0000000..8bede8d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_get_public.yaml
@@ -0,0 +1,22 @@
+!tape
+name: ssh_key_get_public
+interactions:
+- recorded: 2014-04-04T19:47:29.626Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=utf-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=r6p6fl87ftrb1mkwwi0pcak5i;Path=/
+    body: !!binary |-
+      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLXNzaC1rZXktdHlwZT5wdWJsaWM8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
diff --git a/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml b/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml
new file mode 100644
index 0000000..e49f941
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml
@@ -0,0 +1,21 @@
+!tape
+name: ssh_key_list_directory
+interactions:
+- recorded: 2014-04-04T20:33:07.968Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1stwtoatsosy91ca0gcrzde698;Path=/
+    body: application/pgp-keys5publicapplication/octet-stream5contentprivate
diff --git a/src/test/resources/betamax/tapes/ssh_key_list_root.yaml b/src/test/resources/betamax/tapes/ssh_key_list_root.yaml
new file mode 100644
index 0000000..dcefe5e
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_list_root.yaml
@@ -0,0 +1,21 @@
+!tape
+name: ssh_key_list_root
+interactions:
+- recorded: 2014-04-04T20:41:16.501Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=5sj36vytg4y1182mziim4868b;Path=/
+    body: application/octet-stream1679contentprivate
diff --git a/src/test/resources/betamax/tapes/ssh_key_store_private.yaml b/src/test/resources/betamax/tapes/ssh_key_store_private.yaml
new file mode 100644
index 0000000..6ef2def
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_store_private.yaml
@@ -0,0 +1,25 @@
+!tape
+name: ssh_key_store_private
+interactions:
+- recorded: 2014-04-04T19:30:35.367Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem
+    headers:
+      Accept: text/xml
+      Content-Length: '5'
+      Content-Type: application/octet-stream
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+    body: ''
+  response:
+    status: 201
+    headers:
+      Content-Type: application/xml;charset=utf-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=ktmwc4h53xfud6v2ch67x5p9;Path=/
+    body: !!binary |-
+      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2stc3NoLWtleS10eXBlPnByaXZhdGU8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
diff --git a/src/test/resources/betamax/tapes/ssh_key_store_public.yaml b/src/test/resources/betamax/tapes/ssh_key_store_public.yaml
new file mode 100644
index 0000000..8a1b18a
--- /dev/null
+++ b/src/test/resources/betamax/tapes/ssh_key_store_public.yaml
@@ -0,0 +1,25 @@
+!tape
+name: ssh_key_store_public
+interactions:
+- recorded: 2014-04-04T19:34:02.683Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    headers:
+      Accept: text/xml
+      Content-Length: '5'
+      Content-Type: application/pgp-keys
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+    body: ''
+  response:
+    status: 201
+    headers:
+      Content-Type: application/xml;charset=utf-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=2l3g8m0tvwef19jn2bu23bzk6;Path=/
+    body: !!binary |-
+      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLXNzaC1rZXktdHlwZT5wdWJsaWM8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==

From 89452e731a3faea711b70442ae6311ad88e7bee2 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 4 Apr 2014 16:39:52 -0700
Subject: [PATCH 53/89] Update ssh key api todo

---
 src/site/confluence/status.confluence | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 0dfbb3e..3042fa8 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -106,10 +106,10 @@ h2. RunDeck API version 11
 * Delete project - OK
 * Export project archive - OK
 * Import project archive - OK
-* SSH Key upload - *TODO*
-* SSH Key delete - *TODO*
-* SSH Key list - *TODO*
-* SSH Key get - *TODO*
+* SSH Key upload - OK
+* SSH Key delete - OK
+* SSH Key list - OK
+* SSH Key get - OK
 * API Token create - OK
 * API Token list - OK
 * API Token delete - OK

From a59246bdf3e1064abb1086205016fa0481651137 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Wed, 23 Apr 2014 16:18:41 -0700
Subject: [PATCH 54/89] Api sync for key storage

* change ssh-key/ to keys/ in api paths
---
 .../java/org/rundeck/api/RundeckClient.java   |  92 +++++++-------
 ...HKeyResource.java => BaseKeyResource.java} |  23 ++--
 .../{SSHKeyResource.java => KeyResource.java} |   6 +-
 .../api/parser/SSHKeyResourceParser.java      |  11 +-
 .../org/rundeck/api/RundeckClientTest.java    | 112 +++++++++---------
 .../{ssh_key_delete.yaml => key_delete.yaml}  |   4 +-
 ...private.yaml => key_get_data_private.yaml} |   5 +-
 ...a_public.yaml => key_get_data_public.yaml} |   2 +-
 .../betamax/tapes/key_get_private.yaml        |  23 ++++
 ...ey_get_public.yaml => key_get_public.yaml} |   5 +-
 .../betamax/tapes/key_list_directory.yaml     |  21 ++++
 .../betamax/tapes/key_list_root.yaml          |  21 ++++
 ...re_private.yaml => key_store_private.yaml} |   5 +-
 ...tore_public.yaml => key_store_public.yaml} |   5 +-
 .../betamax/tapes/ssh_key_get_private.yaml    |  22 ----
 .../betamax/tapes/ssh_key_list_directory.yaml |  21 ----
 .../betamax/tapes/ssh_key_list_root.yaml      |  21 ----
 17 files changed, 200 insertions(+), 199 deletions(-)
 rename src/main/java/org/rundeck/api/domain/{BaseSSHKeyResource.java => BaseKeyResource.java} (65%)
 rename src/main/java/org/rundeck/api/domain/{SSHKeyResource.java => KeyResource.java} (68%)
 rename src/test/resources/betamax/tapes/{ssh_key_delete.yaml => key_delete.yaml} (86%)
 rename src/test/resources/betamax/tapes/{ssh_key_get_data_private.yaml => key_get_data_private.yaml} (54%)
 rename src/test/resources/betamax/tapes/{ssh_key_get_data_public.yaml => key_get_data_public.yaml} (88%)
 create mode 100644 src/test/resources/betamax/tapes/key_get_private.yaml
 rename src/test/resources/betamax/tapes/{ssh_key_get_public.yaml => key_get_public.yaml} (50%)
 create mode 100644 src/test/resources/betamax/tapes/key_list_directory.yaml
 create mode 100644 src/test/resources/betamax/tapes/key_list_root.yaml
 rename src/test/resources/betamax/tapes/{ssh_key_store_private.yaml => key_store_private.yaml} (50%)
 rename src/test/resources/betamax/tapes/{ssh_key_store_public.yaml => key_store_public.yaml} (53%)
 delete mode 100644 src/test/resources/betamax/tapes/ssh_key_get_private.yaml
 delete mode 100644 src/test/resources/betamax/tapes/ssh_key_list_directory.yaml
 delete mode 100644 src/test/resources/betamax/tapes/ssh_key_list_root.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index b0baa4c..8462dba 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -82,7 +82,7 @@ public class RundeckClient implements Serializable {
     private static final long serialVersionUID = 1L;
     public static final String JOBS_IMPORT = "/jobs/import";
     public static final String STORAGE_ROOT_PATH = "/storage/";
-    public static final String SSH_KEY_PATH = "ssh-key/";
+    public static final String STORAGE_KEYS_PATH = "keys/";
 
     /**
      * Supported version numbers
@@ -2375,18 +2375,18 @@ public class RundeckClient implements Serializable {
     }
 
     /**
-     * Store an SSH key file
-     * @param path ssh key storage path, must start with "ssh-key/"
+     * Store an key file
+     * @param path ssh key storage path, must start with "keys/"
      * @param keyfile key file
      * @param privateKey true to store private key, false to store public key
-     * @return the SSH key resource
+     * @return the key resource
      * @throws RundeckApiException
      */
-    public SSHKeyResource storeSshKey(final String path, final File keyfile, boolean privateKey) throws RundeckApiException{
-        AssertUtil.notNull(path, "path is mandatory to store an SSH key.");
-        AssertUtil.notNull(keyfile, "keyfile is mandatory to store an SSH key.");
-        if (!path.startsWith(SSH_KEY_PATH)) {
-            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+    public KeyResource storeKey(final String path, final File keyfile, boolean privateKey) throws RundeckApiException{
+        AssertUtil.notNull(path, "path is mandatory to store an key.");
+        AssertUtil.notNull(keyfile, "keyfile is mandatory to store an key.");
+        if (!path.startsWith(STORAGE_KEYS_PATH)) {
+            throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
         }
         return new ApiCall(this).post(
                 new ApiPathBuilder(STORAGE_ROOT_PATH, path).content(
@@ -2398,42 +2398,42 @@ public class RundeckClient implements Serializable {
     }
 
     /**
-     * Get metadata for an SSH key file
+     * Get metadata for an key file
      *
-     * @param path ssh key storage path, must start with "ssh-key/"
+     * @param path ssh key storage path, must start with "keys/"
      *
      * @return the ssh key resource
      *
      * @throws RundeckApiException if there is an error, or if the path is a directory not a file
      */
-    public SSHKeyResource getSshKey(final String path) throws RundeckApiException {
-        AssertUtil.notNull(path, "path is mandatory to get an SSH key.");
-        if (!path.startsWith(SSH_KEY_PATH)) {
-            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+    public KeyResource getKey(final String path) throws RundeckApiException {
+        AssertUtil.notNull(path, "path is mandatory to get an key.");
+        if (!path.startsWith(STORAGE_KEYS_PATH)) {
+            throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
         }
-        SSHKeyResource storageResource = new ApiCall(this).get(
+        KeyResource storageResource = new ApiCall(this).get(
                 new ApiPathBuilder(STORAGE_ROOT_PATH, path),
                 new SSHKeyResourceParser("/resource")
         );
         if (storageResource.isDirectory()) {
-            throw new RundeckApiException("SSH Key Path is a directory: " + path);
+            throw new RundeckApiException("Key Path is a directory: " + path);
         }
         return storageResource;
     }
 
     /**
-     * Get content for a public SSH key file
-     * @param path ssh key storage path, must start with "ssh-key/"
+     * Get content for a public key file
+     * @param path ssh key storage path, must start with "keys/"
      * @param out outputstream to write data to
      *
      * @return length of written data
      * @throws RundeckApiException
      */
-    public int getPublicSshKeyContent(final String path, final OutputStream out) throws
+    public int getPublicKeyContent(final String path, final OutputStream out) throws
             RundeckApiException, IOException {
-        AssertUtil.notNull(path, "path is mandatory to get an SSH key.");
-        if (!path.startsWith(SSH_KEY_PATH)) {
-            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+        AssertUtil.notNull(path, "path is mandatory to get an key.");
+        if (!path.startsWith(STORAGE_KEYS_PATH)) {
+            throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
         }
         try {
             return new ApiCall(this).get(
@@ -2443,66 +2443,66 @@ public class RundeckClient implements Serializable {
                     out
             );
         } catch (RundeckApiException.RundeckApiHttpContentTypeException e) {
-            throw new RundeckApiException("Requested SSH Key path was not a Public key: " + path, e);
+            throw new RundeckApiException("Requested Key path was not a Public key: " + path, e);
         }
     }
 
     /**
-     * Get content for a public SSH key file
-     * @param path ssh key storage path, must start with "ssh-key/"
+     * Get content for a public key file
+     * @param path ssh key storage path, must start with "keys/"
      * @param out file to write data to
      * @return length of written data
      * @throws RundeckApiException
      */
-    public int getPublicSshKeyContent(final String path, final File out) throws
+    public int getPublicKeyContent(final String path, final File out) throws
             RundeckApiException, IOException {
         final FileOutputStream fileOutputStream = new FileOutputStream(out);
         try {
-            return getPublicSshKeyContent(path, fileOutputStream);
+            return getPublicKeyContent(path, fileOutputStream);
         }finally {
             fileOutputStream.close();
         }
     }
 
     /**
-     * List contents of root SSH key directory
+     * List contents of root key directory
      *
-     * @return list of SSH key resources
+     * @return list of key resources
      * @throws RundeckApiException
      */
-    public List listSshKeyDirectoryRoot() throws RundeckApiException {
-        return listSshKeyDirectory(SSH_KEY_PATH);
+    public List listKeyDirectoryRoot() throws RundeckApiException {
+        return listKeyDirectory(STORAGE_KEYS_PATH);
     }
     /**
-     * List contents of SSH key directory
+     * List contents of key directory
      *
-     * @param path ssh key storage path, must start with "ssh-key/"
+     * @param path ssh key storage path, must start with "keys/"
      *
      * @throws RundeckApiException if there is an error, or if the path is a file not a directory
      */
-    public List listSshKeyDirectory(final String path) throws RundeckApiException {
-        AssertUtil.notNull(path, "path is mandatory to get an SSH key.");
-        if (!path.startsWith(SSH_KEY_PATH)) {
-            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+    public List listKeyDirectory(final String path) throws RundeckApiException {
+        AssertUtil.notNull(path, "path is mandatory to get an key.");
+        if (!path.startsWith(STORAGE_KEYS_PATH)) {
+            throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
         }
-        SSHKeyResource storageResource = new ApiCall(this).get(
+        KeyResource storageResource = new ApiCall(this).get(
                 new ApiPathBuilder(STORAGE_ROOT_PATH, path),
                 new SSHKeyResourceParser("/resource")
         );
         if(!storageResource.isDirectory()) {
-            throw new RundeckApiException("SSH key path is not a directory path: " + path);
+            throw new RundeckApiException("key path is not a directory path: " + path);
         }
         return storageResource.getDirectoryContents();
     }
 
     /**
-     * Delete an SSH key file
-     * @param path a path to a SSH key file, must start with "ssh-key/"
+     * Delete an key file
+     * @param path a path to a key file, must start with "keys/"
      */
-    public void deleteSshKey(final String path){
-        AssertUtil.notNull(path, "path is mandatory to delete an SSH key.");
-        if (!path.startsWith(SSH_KEY_PATH)) {
-            throw new IllegalArgumentException("SSH key storage path must start with: " + SSH_KEY_PATH);
+    public void deleteKey(final String path){
+        AssertUtil.notNull(path, "path is mandatory to delete an key.");
+        if (!path.startsWith(STORAGE_KEYS_PATH)) {
+            throw new IllegalArgumentException("key storage path must start with: " + STORAGE_KEYS_PATH);
         }
         new ApiCall(this).delete(new ApiPathBuilder(STORAGE_ROOT_PATH, path));
     }
diff --git a/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java b/src/main/java/org/rundeck/api/domain/BaseKeyResource.java
similarity index 65%
rename from src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java
rename to src/main/java/org/rundeck/api/domain/BaseKeyResource.java
index b47d878..c1ed3b7 100644
--- a/src/main/java/org/rundeck/api/domain/BaseSSHKeyResource.java
+++ b/src/main/java/org/rundeck/api/domain/BaseKeyResource.java
@@ -1,21 +1,18 @@
 package org.rundeck.api.domain;
 
-import org.rundeck.api.RundeckClient;
-import org.rundeck.api.parser.StorageResourceParser;
-
 import java.util.ArrayList;
 import java.util.List;
 
 /**
- * BaseSSHKeyResource is ...
+ * BaseKeyResource is ...
  *
  * @author Greg Schueler 
  * @since 2014-04-04
  */
-public class BaseSSHKeyResource extends BaseStorageResource implements SSHKeyResource {
+public class BaseKeyResource extends BaseStorageResource implements KeyResource {
     private boolean privateKey;
 
-    public BaseSSHKeyResource() {
+    public BaseKeyResource() {
     }
 
 
@@ -27,22 +24,22 @@ public class BaseSSHKeyResource extends BaseStorageResource implements SSHKeyRes
         this.privateKey = privateKey;
     }
 
-    ArrayList sshKeyResources = new ArrayList();
+    ArrayList keyResources = new ArrayList();
 
     @Override
     public void setDirectoryContents(List directoryContents) {
         for (StorageResource directoryContent : directoryContents) {
-            sshKeyResources.add(from(directoryContent));
+            keyResources.add(from(directoryContent));
         }
     }
 
     @Override
-    public List getDirectoryContents() {
-        return sshKeyResources;
+    public List getDirectoryContents() {
+        return keyResources;
     }
 
-    public static BaseSSHKeyResource from(final StorageResource source) {
-        final BaseSSHKeyResource baseSshKeyResource = new BaseSSHKeyResource();
+    public static BaseKeyResource from(final StorageResource source) {
+        final BaseKeyResource baseSshKeyResource = new BaseKeyResource();
         baseSshKeyResource.setDirectory(source.isDirectory());
         baseSshKeyResource.setPath(source.getPath());
         baseSshKeyResource.setName(source.getName());
@@ -51,7 +48,7 @@ public class BaseSSHKeyResource extends BaseStorageResource implements SSHKeyRes
         if (!baseSshKeyResource.isDirectory()) {
             baseSshKeyResource.setPrivateKey(
                     null != baseSshKeyResource.getMetadata() && "private".equals(baseSshKeyResource.getMetadata().get
-                            ("Rundeck-ssh-key-type"))
+                            ("Rundeck-key-type"))
             );
         } else if (null != source.getDirectoryContents()) {
             baseSshKeyResource.setDirectoryContents(source.getDirectoryContents());
diff --git a/src/main/java/org/rundeck/api/domain/SSHKeyResource.java b/src/main/java/org/rundeck/api/domain/KeyResource.java
similarity index 68%
rename from src/main/java/org/rundeck/api/domain/SSHKeyResource.java
rename to src/main/java/org/rundeck/api/domain/KeyResource.java
index 33bbbd5..ff1f7e1 100644
--- a/src/main/java/org/rundeck/api/domain/SSHKeyResource.java
+++ b/src/main/java/org/rundeck/api/domain/KeyResource.java
@@ -3,12 +3,12 @@ package org.rundeck.api.domain;
 import java.util.List;
 
 /**
- * SSHKeyResource represents a directory or an SSH key file
+ * KeyResource represents a directory or an SSH key file
  *
  * @author Greg Schueler 
  * @since 2014-04-04
  */
-public interface SSHKeyResource extends StorageResource {
+public interface KeyResource extends StorageResource {
     /**
      * Return true if this is a file and is a private SSH key file.
      * @return
@@ -19,5 +19,5 @@ public interface SSHKeyResource extends StorageResource {
      * Return the list of SSH Key resources if this is a directory
      * @return
      */
-    public List getDirectoryContents();
+    public List getDirectoryContents();
 }
diff --git a/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
index 5485d31..091ab2b 100644
--- a/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
+++ b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
@@ -1,9 +1,8 @@
 package org.rundeck.api.parser;
 
 import org.dom4j.Node;
-import org.rundeck.api.RundeckClient;
-import org.rundeck.api.domain.BaseSSHKeyResource;
-import org.rundeck.api.domain.SSHKeyResource;
+import org.rundeck.api.domain.BaseKeyResource;
+import org.rundeck.api.domain.KeyResource;
 
 /**
  * SSHKeyResourceParser is ...
@@ -11,7 +10,7 @@ import org.rundeck.api.domain.SSHKeyResource;
  * @author Greg Schueler 
  * @since 2014-04-04
  */
-public class SSHKeyResourceParser extends BaseXpathParser implements XmlNodeParser {
+public class SSHKeyResourceParser extends BaseXpathParser implements XmlNodeParser {
     public SSHKeyResourceParser() {
     }
 
@@ -20,7 +19,7 @@ public class SSHKeyResourceParser extends BaseXpathParser implem
     }
 
     @Override
-    public SSHKeyResource parse(Node node) {
-        return BaseSSHKeyResource.from(new StorageResourceParser().parse(node));
+    public KeyResource parse(Node node) {
+        return BaseKeyResource.from(new StorageResourceParser().parse(node));
     }
 }
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index f989e69..fb01b80 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -1284,8 +1284,8 @@ public class RundeckClientTest {
      * Store ssh key
      */
     @Test
-    @Betamax(tape = "ssh_key_store_private", mode = TapeMode.READ_ONLY)
-    public void storeSshKey_private() throws Exception {
+    @Betamax(tape = "key_store_private", mode = TapeMode.READ_ONLY)
+    public void storeKey_private() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
         File temp = File.createTempFile("test-key", ".tmp");
         temp.deleteOnExit();
@@ -1295,26 +1295,26 @@ public class RundeckClientTest {
         }finally {
             out.close();
         }
-        SSHKeyResource storageResource = client.storeSshKey("ssh-key/test/example/file1.pem", temp, true);
+        KeyResource storageResource = client.storeKey("keys/test/example/file1.pem", temp, true);
         Assert.assertNotNull(storageResource);
         Assert.assertFalse(storageResource.isDirectory());
         Assert.assertTrue(storageResource.isPrivateKey());
         Assert.assertEquals("file1.pem", storageResource.getName());
-        Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem",
+        Assert.assertEquals("keys/test/example/file1.pem", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file1.pem",
                 storageResource.getUrl());
         Assert.assertEquals(0, storageResource.getDirectoryContents().size());
         Map metadata = storageResource.getMetadata();
         Assert.assertNotNull(metadata);
         Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type"));
-        Assert.assertEquals("private", metadata.get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("private", metadata.get("Rundeck-key-type"));
     }
     /**
      * Store ssh key
      */
     @Test
-    @Betamax(tape = "ssh_key_store_public", mode = TapeMode.READ_ONLY)
-    public void storeSshKey_public() throws Exception {
+    @Betamax(tape = "key_store_public", mode = TapeMode.READ_ONLY)
+    public void storeKey_public() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
         File temp = File.createTempFile("test-key", ".tmp");
         temp.deleteOnExit();
@@ -1324,76 +1324,76 @@ public class RundeckClientTest {
         }finally {
             out.close();
         }
-        SSHKeyResource storageResource = client.storeSshKey("ssh-key/test/example/file2.pub", temp, false);
+        KeyResource storageResource = client.storeKey("keys/test/example/file2.pub", temp, false);
         Assert.assertNotNull(storageResource);
         Assert.assertFalse(storageResource.isDirectory());
         Assert.assertFalse(storageResource.isPrivateKey());
         Assert.assertEquals("file2.pub", storageResource.getName());
-        Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub",
+        Assert.assertEquals("keys/test/example/file2.pub", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file2.pub",
                 storageResource.getUrl());
         Assert.assertEquals(0, storageResource.getDirectoryContents().size());
         Map metadata = storageResource.getMetadata();
         Assert.assertNotNull(metadata);
         Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type"));
-        Assert.assertEquals("public", metadata.get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("public", metadata.get("Rundeck-key-type"));
     }
     /**
      * get ssh key
      */
     @Test
-    @Betamax(tape = "ssh_key_get_public", mode = TapeMode.READ_ONLY)
-    public void getSshKey_public() throws Exception {
+    @Betamax(tape = "key_get_public", mode = TapeMode.READ_ONLY)
+    public void getKey_public() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
-        SSHKeyResource storageResource = client.getSshKey("ssh-key/test/example/file2.pub");
+        KeyResource storageResource = client.getKey("keys/test/example/file2.pub");
         Assert.assertNotNull(storageResource);
         Assert.assertFalse(storageResource.isDirectory());
         Assert.assertFalse(storageResource.isPrivateKey());
         Assert.assertEquals("file2.pub", storageResource.getName());
-        Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub",
+        Assert.assertEquals("keys/test/example/file2.pub", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file2.pub",
                 storageResource.getUrl());
         Assert.assertEquals(0, storageResource.getDirectoryContents().size());
         Map metadata = storageResource.getMetadata();
         Assert.assertNotNull(metadata);
         Assert.assertEquals("application/pgp-keys", metadata.get("Rundeck-content-type"));
-        Assert.assertEquals("public", metadata.get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("public", metadata.get("Rundeck-key-type"));
     }
     /**
      * get ssh key
      */
     @Test
-    @Betamax(tape = "ssh_key_get_private", mode = TapeMode.READ_ONLY)
-    public void getSshKey_private() throws Exception {
+    @Betamax(tape = "key_get_private", mode = TapeMode.READ_ONLY)
+    public void getKey_private() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
-        SSHKeyResource storageResource = client.getSshKey("ssh-key/test/example/file1.pem");
+        KeyResource storageResource = client.getKey("keys/test/example/file1.pem");
         Assert.assertNotNull(storageResource);
         Assert.assertFalse(storageResource.isDirectory());
         Assert.assertTrue(storageResource.isPrivateKey());
         Assert.assertEquals("file1.pem", storageResource.getName());
-        Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem",
+        Assert.assertEquals("keys/test/example/file1.pem", storageResource.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file1.pem",
                 storageResource.getUrl());
         Assert.assertEquals(0, storageResource.getDirectoryContents().size());
         Map metadata = storageResource.getMetadata();
         Assert.assertNotNull(metadata);
         Assert.assertEquals("application/octet-stream", metadata.get("Rundeck-content-type"));
-        Assert.assertEquals("private", metadata.get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("private", metadata.get("Rundeck-key-type"));
     }
     /**
      * get ssh key data
      */
     @Test
-    @Betamax(tape = "ssh_key_get_data_private", mode = TapeMode.READ_ONLY)
-    public void getSshKeyData_private() throws Exception {
+    @Betamax(tape = "key_get_data_private", mode = TapeMode.READ_ONLY)
+    public void getKeyData_private() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
         File temp = File.createTempFile("test-key", ".tmp");
         temp.deleteOnExit();
         try {
-            int data = client.getPublicSshKeyContent("ssh-key/test/example/file1.pem", temp);
+            int data = client.getPublicKeyContent("keys/test/example/file1.pem", temp);
             Assert.fail("expected failure");
         } catch (RundeckApiException e) {
-            Assert.assertEquals("Requested SSH Key path was not a Public key: ssh-key/test/example/file1.pem",
+            Assert.assertEquals("Requested Key path was not a Public key: keys/test/example/file1.pem",
                     e.getMessage());
         }
     }
@@ -1401,72 +1401,72 @@ public class RundeckClientTest {
      * get ssh key data
      */
     @Test
-    @Betamax(tape = "ssh_key_get_data_public", mode = TapeMode.READ_ONLY)
-    public void getSshKeyData_public() throws Exception {
+    @Betamax(tape = "key_get_data_public", mode = TapeMode.READ_ONLY)
+    public void getKeyData_public() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
         File temp = File.createTempFile("test-key", ".tmp");
         temp.deleteOnExit();
-        int length = client.getPublicSshKeyContent("ssh-key/test/example/file2.pub", temp);
+        int length = client.getPublicKeyContent("keys/test/example/file2.pub", temp);
         Assert.assertEquals(5, length);
     }
     /**
      * list directory
      */
     @Test
-    @Betamax(tape = "ssh_key_list_directory", mode = TapeMode.READ_ONLY)
-    public void listSshKeyDirectory() throws Exception {
+    @Betamax(tape = "key_list_directory", mode = TapeMode.READ_ONLY)
+    public void listKeyDirectory() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
-        List list = client.listSshKeyDirectory("ssh-key/test/example");
+        List list = client.listKeyDirectory("keys/test/example");
         Assert.assertEquals(2, list.size());
-        SSHKeyResource storageResource1 = list.get(0);
-        SSHKeyResource storageResource2 = list.get(1);
+        KeyResource storageResource1 = list.get(0);
+        KeyResource storageResource2 = list.get(1);
 
         Assert.assertFalse(storageResource2.isDirectory());
         Assert.assertTrue(storageResource2.isPrivateKey());
         Assert.assertEquals("file1.pem", storageResource2.getName());
-        Assert.assertEquals("ssh-key/test/example/file1.pem", storageResource2.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file1.pem", storageResource2.getUrl());
+        Assert.assertEquals("keys/test/example/file1.pem", storageResource2.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file1.pem", storageResource2.getUrl());
         Assert.assertNotNull(storageResource2.getMetadata());
 
         Assert.assertEquals("application/octet-stream", storageResource2.getMetadata().get("Rundeck-content-type"));
-        Assert.assertEquals("private", storageResource2.getMetadata().get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("private", storageResource2.getMetadata().get("Rundeck-key-type"));
 
         Assert.assertFalse(storageResource1.isDirectory());
         Assert.assertFalse(storageResource1.isPrivateKey());
         Assert.assertEquals("file2.pub", storageResource1.getName());
-        Assert.assertEquals("ssh-key/test/example/file2.pub", storageResource1.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test/example/file2.pub",
+        Assert.assertEquals("keys/test/example/file2.pub", storageResource1.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test/example/file2.pub",
                 storageResource1.getUrl());
         Assert.assertNotNull(storageResource1.getMetadata());
         Assert.assertEquals("application/pgp-keys", storageResource1.getMetadata().get("Rundeck-content-type"));
-        Assert.assertEquals("public", storageResource1.getMetadata().get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("public", storageResource1.getMetadata().get("Rundeck-key-type"));
     }
     /**
      * list root
      */
     @Test
-    @Betamax(tape = "ssh_key_list_root", mode = TapeMode.READ_ONLY)
-    public void listSshKeyDirectoryRoot() throws Exception {
+    @Betamax(tape = "key_list_root", mode = TapeMode.READ_ONLY)
+    public void listKeyDirectoryRoot() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
-        List list = client.listSshKeyDirectoryRoot();
+        List list = client.listKeyDirectoryRoot();
         Assert.assertEquals(2, list.size());
-        SSHKeyResource storageResource0 = list.get(0);
-        SSHKeyResource storageResource1 = list.get(1);
+        KeyResource storageResource0 = list.get(0);
+        KeyResource storageResource1 = list.get(1);
 
         Assert.assertFalse(storageResource0.isDirectory());
         Assert.assertTrue(storageResource0.isPrivateKey());
         Assert.assertEquals("test1.pem", storageResource0.getName());
-        Assert.assertEquals("ssh-key/test1.pem", storageResource0.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test1.pem", storageResource0.getUrl());
+        Assert.assertEquals("keys/test1.pem", storageResource0.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test1.pem", storageResource0.getUrl());
         Assert.assertNotNull(storageResource0.getMetadata());
 
         Assert.assertEquals("application/octet-stream", storageResource0.getMetadata().get("Rundeck-content-type"));
-        Assert.assertEquals("private", storageResource0.getMetadata().get("Rundeck-ssh-key-type"));
+        Assert.assertEquals("private", storageResource0.getMetadata().get("Rundeck-key-type"));
 
         Assert.assertTrue(storageResource1.toString(), storageResource1.isDirectory());
         Assert.assertEquals(null, storageResource1.getName());
-        Assert.assertEquals("ssh-key/test", storageResource1.getPath());
-        Assert.assertEquals("http://dignan.local:4440/api/11/storage/ssh-key/test",
+        Assert.assertEquals("keys/test", storageResource1.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test",
                 storageResource1.getUrl());
         Assert.assertNull(storageResource1.getMetadata());
 
@@ -1477,13 +1477,13 @@ public class RundeckClientTest {
      * delete ssh key
      */
     @Test
-    @Betamax(tape = "ssh_key_delete", mode = TapeMode.READ_ONLY)
-    public void deleteSshKey() throws Exception {
+    @Betamax(tape = "key_delete", mode = TapeMode.READ_ONLY)
+    public void deleteKey() throws Exception {
         final RundeckClient client = createClient(TEST_TOKEN_7, 11);
-        client.deleteSshKey("ssh-key/test/example/file2.pub");
+        client.deleteKey("keys/test/example/file2.pub");
 
         try {
-            client.getSshKey("ssh-key/test/example/file2.pub");
+            client.getKey("keys/test/example/file2.pub");
             Assert.fail("expected failure");
         } catch (RundeckApiException.RundeckApiHttpStatusException e) {
             Assert.assertEquals(404,e.getStatusCode());
diff --git a/src/test/resources/betamax/tapes/ssh_key_delete.yaml b/src/test/resources/betamax/tapes/key_delete.yaml
similarity index 86%
rename from src/test/resources/betamax/tapes/ssh_key_delete.yaml
rename to src/test/resources/betamax/tapes/key_delete.yaml
index fd46eae..b52ed7f 100644
--- a/src/test/resources/betamax/tapes/ssh_key_delete.yaml
+++ b/src/test/resources/betamax/tapes/key_delete.yaml
@@ -4,7 +4,7 @@ interactions:
 - recorded: 2014-04-04T23:16:02.256Z
   request:
     method: DELETE
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
     headers:
       Host: rundeck.local:4440
       Proxy-Connection: Keep-Alive
@@ -20,7 +20,7 @@ interactions:
 - recorded: 2014-04-04T23:16:02.372Z
   request:
     method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
     headers:
       Accept: text/xml
       Host: rundeck.local:4440
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml b/src/test/resources/betamax/tapes/key_get_data_private.yaml
similarity index 54%
rename from src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml
rename to src/test/resources/betamax/tapes/key_get_data_private.yaml
index e5e742b..ef0f58f 100644
--- a/src/test/resources/betamax/tapes/ssh_key_get_data_private.yaml
+++ b/src/test/resources/betamax/tapes/key_get_data_private.yaml
@@ -4,7 +4,7 @@ interactions:
 - recorded: 2014-04-04T19:50:59.155Z
   request:
     method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file1.pem
     headers:
       Accept: application/pgp-keys
       Host: rundeck.local:4440
@@ -19,4 +19,5 @@ interactions:
       Server: Jetty(7.6.0.v20120127)
       Set-Cookie: JSESSIONID=1gzu37lkjr0fitxhf5fgkgsfu;Path=/
     body: !!binary |-
-      eyJwYXRoIjoic3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtIiwidHlwZSI6ImZpbGUiLCJuYW1lIjoiZmlsZTEucGVtIiwidXJsIjoiaHR0cDovL2RpZ25hbi5sb2NhbDo0NDQwL2FwaS8xMS9zdG9yYWdlL3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbSIsIm1ldGEiOnsiUnVuZGVjay1jb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJSdW5kZWNrLWNvbnRlbnQtc2l6ZSI6IjUiLCJSdW5kZWNrLWNvbnRlbnQtbWFzayI6ImNvbnRlbnQiLCJSdW5kZWNrLXNzaC1rZXktdHlwZSI6InByaXZhdGUifX0=
+      eyJwYXRoIjoia2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtIiwidHlwZSI6ImZpbGUiLCJuYW1lIjoiZmlsZTEucGVtIiwidXJsIjoiaHR0cDovL2RpZ25hbi5sb2NhbDo0NDQwL2FwaS8xMS9zdG9yYWdlL2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbSIsIm1ldGEiOnsiUnVuZGVjay1jb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJSdW5kZWNrLWNvbnRlbnQtc2l6ZSI6IjUiLCJSdW5kZWNrLWNvbnRlbnQtbWFzayI6ImNvbnRlbnQiLCJSdW5kZWNrLWtleS10eXBlIjoicHJpdmF0ZSJ9fQo=
+
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml b/src/test/resources/betamax/tapes/key_get_data_public.yaml
similarity index 88%
rename from src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml
rename to src/test/resources/betamax/tapes/key_get_data_public.yaml
index 1874e71..054599f 100644
--- a/src/test/resources/betamax/tapes/ssh_key_get_data_public.yaml
+++ b/src/test/resources/betamax/tapes/key_get_data_public.yaml
@@ -4,7 +4,7 @@ interactions:
 - recorded: 2014-04-04T20:20:44.331Z
   request:
     method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
     headers:
       Accept: application/pgp-keys
       Host: rundeck.local:4440
diff --git a/src/test/resources/betamax/tapes/key_get_private.yaml b/src/test/resources/betamax/tapes/key_get_private.yaml
new file mode 100644
index 0000000..57802a8
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_private.yaml
@@ -0,0 +1,23 @@
+!tape
+name: ssh_key_get_private
+interactions:
+- recorded: 2014-04-04T19:47:29.880Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file1.pem
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=utf-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=nc5p0he3nw19e4gegidc4bs7;Path=/
+    body: !!binary |-
+      PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2sta2V5LXR5cGU+cHJpdmF0ZTwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4K
+
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_public.yaml b/src/test/resources/betamax/tapes/key_get_public.yaml
similarity index 50%
rename from src/test/resources/betamax/tapes/ssh_key_get_public.yaml
rename to src/test/resources/betamax/tapes/key_get_public.yaml
index 8bede8d..32c2510 100644
--- a/src/test/resources/betamax/tapes/ssh_key_get_public.yaml
+++ b/src/test/resources/betamax/tapes/key_get_public.yaml
@@ -4,7 +4,7 @@ interactions:
 - recorded: 2014-04-04T19:47:29.626Z
   request:
     method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
     headers:
       Accept: text/xml
       Host: rundeck.local:4440
@@ -19,4 +19,5 @@ interactions:
       Server: Jetty(7.6.0.v20120127)
       Set-Cookie: JSESSIONID=r6p6fl87ftrb1mkwwi0pcak5i;Path=/
     body: !!binary |-
-      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLXNzaC1rZXktdHlwZT5wdWJsaWM8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
+      PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLWtleS10eXBlPnB1YmxpYzwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4N
+
diff --git a/src/test/resources/betamax/tapes/key_list_directory.yaml b/src/test/resources/betamax/tapes/key_list_directory.yaml
new file mode 100644
index 0000000..fb4c6f6
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_list_directory.yaml
@@ -0,0 +1,21 @@
+!tape
+name: key_list_directory
+interactions:
+- recorded: 2014-04-04T20:33:07.968Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1stwtoatsosy91ca0gcrzde698;Path=/
+    body: application/pgp-keys5publicapplication/octet-stream5contentprivate
diff --git a/src/test/resources/betamax/tapes/key_list_root.yaml b/src/test/resources/betamax/tapes/key_list_root.yaml
new file mode 100644
index 0000000..b040c3e
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_list_root.yaml
@@ -0,0 +1,21 @@
+!tape
+name: key_list_root
+interactions:
+- recorded: 2014-04-04T20:41:16.501Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/storage/keys/
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=5sj36vytg4y1182mziim4868b;Path=/
+    body: application/octet-stream1679contentprivate
diff --git a/src/test/resources/betamax/tapes/ssh_key_store_private.yaml b/src/test/resources/betamax/tapes/key_store_private.yaml
similarity index 50%
rename from src/test/resources/betamax/tapes/ssh_key_store_private.yaml
rename to src/test/resources/betamax/tapes/key_store_private.yaml
index 6ef2def..1a5d0b0 100644
--- a/src/test/resources/betamax/tapes/ssh_key_store_private.yaml
+++ b/src/test/resources/betamax/tapes/key_store_private.yaml
@@ -4,7 +4,7 @@ interactions:
 - recorded: 2014-04-04T19:30:35.367Z
   request:
     method: POST
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file1.pem
     headers:
       Accept: text/xml
       Content-Length: '5'
@@ -22,4 +22,5 @@ interactions:
       Server: Jetty(7.6.0.v20120127)
       Set-Cookie: JSESSIONID=ktmwc4h53xfud6v2ch67x5p9;Path=/
     body: !!binary |-
-      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2stc3NoLWtleS10eXBlPnByaXZhdGU8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
+      PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2sta2V5LXR5cGU+cHJpdmF0ZTwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4N
+
diff --git a/src/test/resources/betamax/tapes/ssh_key_store_public.yaml b/src/test/resources/betamax/tapes/key_store_public.yaml
similarity index 53%
rename from src/test/resources/betamax/tapes/ssh_key_store_public.yaml
rename to src/test/resources/betamax/tapes/key_store_public.yaml
index 8a1b18a..fdd7638 100644
--- a/src/test/resources/betamax/tapes/ssh_key_store_public.yaml
+++ b/src/test/resources/betamax/tapes/key_store_public.yaml
@@ -4,7 +4,7 @@ interactions:
 - recorded: 2014-04-04T19:34:02.683Z
   request:
     method: POST
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file2.pub
+    uri: http://rundeck.local:4440/api/11/storage/keys/test/example/file2.pub
     headers:
       Accept: text/xml
       Content-Length: '5'
@@ -22,4 +22,5 @@ interactions:
       Server: Jetty(7.6.0.v20120127)
       Set-Cookie: JSESSIONID=2l3g8m0tvwef19jn2bu23bzk6;Path=/
     body: !!binary |-
-      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLXNzaC1rZXktdHlwZT5wdWJsaWM8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
+      PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLWtleS10eXBlPnB1YmxpYzwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4K
+
diff --git a/src/test/resources/betamax/tapes/ssh_key_get_private.yaml b/src/test/resources/betamax/tapes/ssh_key_get_private.yaml
deleted file mode 100644
index fa0ff2c..0000000
--- a/src/test/resources/betamax/tapes/ssh_key_get_private.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-!tape
-name: ssh_key_get_private
-interactions:
-- recorded: 2014-04-04T19:47:29.880Z
-  request:
-    method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example/file1.pem
-    headers:
-      Accept: text/xml
-      Host: rundeck.local:4440
-      Proxy-Connection: Keep-Alive
-      User-Agent: RunDeck API Java Client 11
-      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
-  response:
-    status: 200
-    headers:
-      Content-Type: application/xml;charset=utf-8
-      Expires: Thu, 01 Jan 1970 00:00:00 GMT
-      Server: Jetty(7.6.0.v20120127)
-      Set-Cookie: JSESSIONID=nc5p0he3nw19e4gegidc4bs7;Path=/
-    body: !!binary |-
-      PHJlc291cmNlIHBhdGg9J3NzaC1rZXkvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uvc3NoLWtleS90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2stc3NoLWtleS10eXBlPnByaXZhdGU8L1J1bmRlY2stc3NoLWtleS10eXBlPjwvcmVzb3VyY2UtbWV0YT48L3Jlc291cmNlPg==
diff --git a/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml b/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml
deleted file mode 100644
index e49f941..0000000
--- a/src/test/resources/betamax/tapes/ssh_key_list_directory.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-!tape
-name: ssh_key_list_directory
-interactions:
-- recorded: 2014-04-04T20:33:07.968Z
-  request:
-    method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/test/example
-    headers:
-      Accept: text/xml
-      Host: rundeck.local:4440
-      Proxy-Connection: Keep-Alive
-      User-Agent: RunDeck API Java Client 11
-      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
-  response:
-    status: 200
-    headers:
-      Content-Type: text/html;charset=UTF-8
-      Expires: Thu, 01 Jan 1970 00:00:00 GMT
-      Server: Jetty(7.6.0.v20120127)
-      Set-Cookie: JSESSIONID=1stwtoatsosy91ca0gcrzde698;Path=/
-    body: application/pgp-keys5publicapplication/octet-stream5contentprivate
diff --git a/src/test/resources/betamax/tapes/ssh_key_list_root.yaml b/src/test/resources/betamax/tapes/ssh_key_list_root.yaml
deleted file mode 100644
index dcefe5e..0000000
--- a/src/test/resources/betamax/tapes/ssh_key_list_root.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-!tape
-name: ssh_key_list_root
-interactions:
-- recorded: 2014-04-04T20:41:16.501Z
-  request:
-    method: GET
-    uri: http://rundeck.local:4440/api/11/storage/ssh-key/
-    headers:
-      Accept: text/xml
-      Host: rundeck.local:4440
-      Proxy-Connection: Keep-Alive
-      User-Agent: RunDeck API Java Client 11
-      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
-  response:
-    status: 200
-    headers:
-      Content-Type: text/html;charset=UTF-8
-      Expires: Thu, 01 Jan 1970 00:00:00 GMT
-      Server: Jetty(7.6.0.v20120127)
-      Set-Cookie: JSESSIONID=5sj36vytg4y1182mziim4868b;Path=/
-    body: application/octet-stream1679contentprivate

From 0191bd34ff63d722271170b632241006168432af Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Wed, 23 Apr 2014 16:37:11 -0700
Subject: [PATCH 55/89] Update status and changes docs

---
 src/changes/changes.xml               | 16 ++++++++++++++++
 src/site/confluence/status.confluence |  8 ++++----
 2 files changed, 20 insertions(+), 4 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ba52cb2..825c4ca 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -22,6 +22,22 @@
     Vincent Behar
   
   
+      
+          Project creation
+          Get Project configuration
+          Set Project configuration
+          Get/Set Project configuration keys
+          Delete project
+          Export project archive
+          Import project archive
+          Key file upload
+          Key file delete
+          Key file list
+          Key file get
+          API Token create
+          API Token list
+          API Token delete
+      
       
           
               Execution State - Retrieve workflow step and node state information
diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 3042fa8..39a2185 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -106,10 +106,10 @@ h2. RunDeck API version 11
 * Delete project - OK
 * Export project archive - OK
 * Import project archive - OK
-* SSH Key upload - OK
-* SSH Key delete - OK
-* SSH Key list - OK
-* SSH Key get - OK
+* Key file upload - OK
+* Key file delete - OK
+* Key file list - OK
+* Key file get - OK
 * API Token create - OK
 * API Token list - OK
 * API Token delete - OK

From 7146aca1297fcb1f4d186b35d02800df56185ce7 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Wed, 23 Apr 2014 17:51:51 -0700
Subject: [PATCH 56/89] Update version to 11.0-SNAPSHOT

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index d284be1..eebf968 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  10.1-SNAPSHOT
+  11.0-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From d3cad78f3d90a0eb00e8c0883e7d1336b9561354 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Wed, 23 Apr 2014 18:43:00 -0700
Subject: [PATCH 57/89] [maven-release-plugin] prepare release
 rundeck-api-java-client-11.0-redo3

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index eebf968..a39e4f7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  11.0-SNAPSHOT
+  11.0
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From 7b6259b66b011c34cbc014e67cdc1a8882c98a22 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Wed, 23 Apr 2014 18:43:04 -0700
Subject: [PATCH 58/89] [maven-release-plugin] prepare for next development
 iteration

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index a39e4f7..722956b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  11.0
+  11.1-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From bbe599b915fa662a088cf559579f165a33c45b22 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 08:49:25 -0800
Subject: [PATCH 59/89] Add todo

---
 src/site/confluence/status.confluence | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 39a2185..c161cf4 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -113,3 +113,11 @@ h2. RunDeck API version 11
 * API Token create - OK
 * API Token list - OK
 * API Token delete - OK
+
+h2. Rundeck API version 12
+
+[Documentation of the RunDeck API version 12|http://rundeck.org/2.2.0/api/index.html]
+
+* Bulk delete executions - TBD
+* Delete execution - TBD
+* Delete all executions for a job - TBD

From a7c0c22699475fd31ceb283b8e35e5d516f7135f Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 09:32:30 -0800
Subject: [PATCH 60/89] Add bulk Delete executions endpoint

---
 .../java/org/rundeck/api/RundeckClient.java   | 28 ++++++-
 .../api/domain/DeleteExecutionsResponse.java  | 84 +++++++++++++++++++
 .../generator/DeleteExecutionsGenerator.java  | 38 +++++++++
 .../DeleteExecutionsResponseParser.java       | 52 ++++++++++++
 .../org/rundeck/api/RundeckClientTest.java    | 82 ++++++++++++++++++
 .../tapes/delete_executions_mixed.yaml        | 26 ++++++
 .../tapes/delete_executions_success.yaml      | 26 ++++++
 .../tapes/delete_executions_unauthorized.yaml | 26 ++++++
 8 files changed, 361 insertions(+), 1 deletion(-)
 create mode 100644 src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java
 create mode 100644 src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java
 create mode 100644 src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java
 create mode 100644 src/test/resources/betamax/tapes/delete_executions_mixed.yaml
 create mode 100644 src/test/resources/betamax/tapes/delete_executions_success.yaml
 create mode 100644 src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 8462dba..93ced66 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -23,6 +23,7 @@ import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
 import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.domain.*;
 import org.rundeck.api.domain.RundeckExecution.ExecutionStatus;
+import org.rundeck.api.generator.DeleteExecutionsGenerator;
 import org.rundeck.api.generator.ProjectConfigGenerator;
 import org.rundeck.api.generator.ProjectConfigPropertyGenerator;
 import org.rundeck.api.generator.ProjectGenerator;
@@ -95,6 +96,7 @@ public class RundeckClient implements Serializable {
         V9(9),
         V10(10),
         V11(11),
+        V12(12),
         ;
 
         private int versionNumber;
@@ -108,7 +110,7 @@ public class RundeckClient implements Serializable {
         }
     }
     /** Version of the API supported */
-    public static final transient int API_VERSION = Version.V11.getVersionNumber();
+    public static final transient int API_VERSION = Version.V12.getVersionNumber();
 
     private static final String API = "/api/";
 
@@ -1643,6 +1645,30 @@ public class RundeckClient implements Serializable {
         return new ApiCall(this).get(apiPath, new AbortParser(rootXpath()+"/abort"));
     }
 
+    /**
+     * Delete a set of executions, identified by the given IDs
+     *
+     * @param executionIds set of identifiers for the executions - mandatory
+     * @return a {@link RundeckExecution} instance - won't be null
+     * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the executionId is null
+     */
+    public DeleteExecutionsResponse deleteExecutions(Set executionIds)
+            throws RundeckApiException, RundeckApiLoginException,
+                   RundeckApiTokenException, IllegalArgumentException
+    {
+        AssertUtil.notNull(executionIds, "executionIds is mandatory to abort an execution !");
+        final ApiPathBuilder apiPath = new ApiPathBuilder("/executions/delete").xml(
+                new DeleteExecutionsGenerator(executionIds)
+        );
+        return new ApiCall(this).post(
+                apiPath,
+                new DeleteExecutionsResponseParser( rootXpath() + "/deleteExecutions")
+        );
+    }
+
     /*
      * History
      */
diff --git a/src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java b/src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java
new file mode 100644
index 0000000..6d1f53b
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/DeleteExecutionsResponse.java
@@ -0,0 +1,84 @@
+package org.rundeck.api.domain;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * DeleteExecutionsResponse is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-11-06
+ */
+public class DeleteExecutionsResponse implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private int                 failedCount;
+    private int                 successCount;
+    private boolean             allsuccessful;
+    private int                 requestCount;
+    private List failures;
+
+    public int getFailedCount() {
+        return failedCount;
+    }
+
+    public void setFailedCount(final int failedCount) {
+        this.failedCount = failedCount;
+    }
+
+    public int getSuccessCount() {
+        return successCount;
+    }
+
+    public void setSuccessCount(final int successCount) {
+        this.successCount = successCount;
+    }
+
+    public boolean isAllsuccessful() {
+        return allsuccessful;
+    }
+
+    public void setAllsuccessful(final boolean allsuccessful) {
+        this.allsuccessful = allsuccessful;
+    }
+
+    public int getRequestCount() {
+        return requestCount;
+    }
+
+    public void setRequestCount(final int requestCount) {
+        this.requestCount = requestCount;
+    }
+
+    public List getFailures() {
+        return failures;
+    }
+
+    public void setFailures(final List failures) {
+        this.failures = failures;
+    }
+
+    public static class DeleteFailure implements Serializable{
+
+        private static final long serialVersionUID = 1L;
+        private Long executionId;
+        private String message;
+
+        public Long getExecutionId() {
+            return executionId;
+        }
+
+        public void setExecutionId(final Long executionId) {
+            this.executionId = executionId;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        public void setMessage(final String message) {
+            this.message = message;
+        }
+    }
+}
diff --git a/src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java b/src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java
new file mode 100644
index 0000000..8efd2bf
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/DeleteExecutionsGenerator.java
@@ -0,0 +1,38 @@
+package org.rundeck.api.generator;
+
+import org.dom4j.DocumentFactory;
+import org.dom4j.Element;
+import org.rundeck.api.domain.ProjectConfig;
+
+import java.util.List;
+import java.util.Set;
+
+/**
+ * DeleteExecutionsGenerator is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-11-06
+ */
+public class DeleteExecutionsGenerator extends BaseDocGenerator {
+    private Set executionIds;
+
+    public DeleteExecutionsGenerator(final Set executionIds) {
+        this.executionIds = executionIds;
+    }
+
+    @Override public Element generateXmlElement() {
+        Element rootElem = DocumentFactory.getInstance().createElement("executions");
+        for (Long executionId : executionIds) {
+            rootElem.addElement("execution").addAttribute("id", Long.toString(executionId));
+        }
+        return rootElem;
+    }
+
+    public Set getExecutionIds() {
+        return executionIds;
+    }
+
+    public void setExecutionIds(final Set executionIds) {
+        this.executionIds = executionIds;
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java b/src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java
new file mode 100644
index 0000000..c3de8af
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/DeleteExecutionsResponseParser.java
@@ -0,0 +1,52 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.DeleteExecutionsResponse;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * DeleteExecutionsResponseParser is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-11-06
+ */
+public class DeleteExecutionsResponseParser implements XmlNodeParser {
+    private String xpath;
+
+    public DeleteExecutionsResponseParser(final String xpath) {
+        this.xpath = xpath;
+    }
+
+    @Override public DeleteExecutionsResponse parseXmlNode(final Node node) {
+        final Node baseNode = xpath != null ? node.selectSingleNode(xpath) : node;
+
+        final DeleteExecutionsResponse response = new DeleteExecutionsResponse();
+        response.setAllsuccessful(Boolean.parseBoolean(baseNode.valueOf("@allsuccessful")));
+        response.setRequestCount(Integer.parseInt(baseNode.valueOf("@requestCount")));
+        response.setSuccessCount(Integer.parseInt(baseNode.valueOf("successful/@count")));
+
+        final Node failedNode = baseNode.selectSingleNode("failed");
+        //parse failures
+        final List failures = new ArrayList
+                ();
+        int failedCount = 0;
+        if (null != failedNode) {
+            failedCount = Integer.parseInt(baseNode.valueOf("failed/@count"));
+            final List list = baseNode.selectNodes("failed/execution");
+
+            for (final Object o : list) {
+                final Node execNode = (Node) o;
+                final DeleteExecutionsResponse.DeleteFailure deleteFailure =
+                        new DeleteExecutionsResponse.DeleteFailure();
+                deleteFailure.setExecutionId(Long.parseLong(execNode.valueOf("@id")));
+                deleteFailure.setMessage(execNode.valueOf("@message"));
+                failures.add(deleteFailure);
+            }
+        }
+        response.setFailedCount(failedCount);
+        response.setFailures(failures);
+        return response;
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index fb01b80..27fa3eb 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -56,6 +56,7 @@ public class RundeckClientTest {
     public static final String TEST_TOKEN_5 = "C3O6d5O98Kr6Dpv71sdE4ERdCuU12P6d";
     public static final String TEST_TOKEN_6 = "Do4d3NUD5DKk21DR4sNK755RcPk618vn";
     public static final String TEST_TOKEN_7 = "8Dp9op111ER6opsDRkddvE86K9sE499s";
+    public static final String TEST_TOKEN_8 = "GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI";
 
     @Rule
     public Recorder recorder = new Recorder();
@@ -1490,6 +1491,87 @@ public class RundeckClientTest {
         }
     }
 
+    /**
+     * delete executions with failure
+     */
+    @Test
+    @Betamax(tape = "delete_executions_unauthorized", mode = TapeMode.READ_ONLY)
+    public void deleteExecutionsUnauthorized() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        final DeleteExecutionsResponse response = client.deleteExecutions(
+                new HashSet() {{
+                    add(640L);
+                    add(641L);
+                }}
+        );
+        Assert.assertEquals(2, response.getRequestCount());
+        Assert.assertEquals(0, response.getSuccessCount());
+        Assert.assertEquals(2, response.getFailedCount());
+        Assert.assertFalse(response.isAllsuccessful());
+        Assert.assertNotNull(response.getFailures());
+        Assert.assertEquals(2, response.getFailures().size());
+        Assert.assertEquals(Long.valueOf(641L), response.getFailures().get(0).getExecutionId());
+        Assert.assertEquals(
+                "Unauthorized: Delete execution in project test",
+                response.getFailures().get(0).getMessage()
+        );
+        Assert.assertEquals(Long.valueOf(640L), response.getFailures().get(1).getExecutionId());
+        Assert.assertEquals(
+                "Unauthorized: Delete execution in project test",
+                response.getFailures().get(1).getMessage()
+        );
+    }
+    /**
+     * delete executions with success
+     */
+    @Test
+    @Betamax(tape = "delete_executions_success", mode = TapeMode.READ_ONLY)
+    public void deleteExecutionsSuccess() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        final DeleteExecutionsResponse response = client.deleteExecutions(
+                new HashSet() {{
+                    add(640L);
+                    add(641L);
+                }}
+        );
+        Assert.assertEquals(2, response.getRequestCount());
+        Assert.assertEquals(2, response.getSuccessCount());
+        Assert.assertEquals(0, response.getFailedCount());
+        Assert.assertTrue(response.isAllsuccessful());
+        Assert.assertNotNull(response.getFailures());
+        Assert.assertEquals(0, response.getFailures().size());
+    }
+    /**
+     * delete executions mixed success
+     */
+    @Test
+    @Betamax(tape = "delete_executions_mixed", mode = TapeMode.READ_ONLY)
+    public void deleteExecutionsMixed() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        final DeleteExecutionsResponse response = client.deleteExecutions(
+                new HashSet() {{
+                    add(642L);
+                    add(640L);
+                    add(1640L);
+                }}
+        );
+        Assert.assertEquals(3, response.getRequestCount());
+        Assert.assertEquals(1, response.getSuccessCount());
+        Assert.assertEquals(2, response.getFailedCount());
+        Assert.assertFalse(response.isAllsuccessful());
+        Assert.assertNotNull(response.getFailures());
+        Assert.assertEquals(2, response.getFailures().size());
+        Assert.assertEquals(Long.valueOf(1640L), response.getFailures().get(0).getExecutionId());
+        Assert.assertEquals(
+                "Execution Not found: 1640",
+                response.getFailures().get(0).getMessage()
+        );
+        Assert.assertEquals(Long.valueOf(640L), response.getFailures().get(1).getExecutionId());
+        Assert.assertEquals(
+                "Execution Not found: 640",
+                response.getFailures().get(1).getMessage()
+        );
+    }
     @Before
     public void setUp() throws Exception {
         // not that you can put whatever here, because we don't actually connect to the RunDeck instance
diff --git a/src/test/resources/betamax/tapes/delete_executions_mixed.yaml b/src/test/resources/betamax/tapes/delete_executions_mixed.yaml
new file mode 100644
index 0000000..c21657d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_executions_mixed.yaml
@@ -0,0 +1,26 @@
+!tape
+name: delete_executions_mixed
+interactions:
+- recorded: 2014-11-06T17:29:44.266Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/12/executions/delete
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=smlj6wcfe4yccemt6bptnmgy;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PSczJyBhbGxzdWNjZXNzZnVsPSdmYWxzZSc+CiAgPHN1Y2Nlc3NmdWwgY291bnQ9JzEnIC8+CiAgPGZhaWxlZCBjb3VudD0nMic+CiAgICA8ZXhlY3V0aW9uIGlkPScxNjQwJyBtZXNzYWdlPSdFeGVjdXRpb24gTm90IGZvdW5kOiAxNjQwJyAvPgogICAgPGV4ZWN1dGlvbiBpZD0nNjQwJyBtZXNzYWdlPSdFeGVjdXRpb24gTm90IGZvdW5kOiA2NDAnIC8+CiAgPC9mYWlsZWQ+CjwvZGVsZXRlRXhlY3V0aW9ucz4=
diff --git a/src/test/resources/betamax/tapes/delete_executions_success.yaml b/src/test/resources/betamax/tapes/delete_executions_success.yaml
new file mode 100644
index 0000000..d412342
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_executions_success.yaml
@@ -0,0 +1,26 @@
+!tape
+name: delete_executions_success
+interactions:
+- recorded: 2014-11-06T17:24:56.487Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/12/executions/delete
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1xueq6cjfrsnxjn43hcfadqyk;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PScyJyBhbGxzdWNjZXNzZnVsPSd0cnVlJz4KICA8c3VjY2Vzc2Z1bCBjb3VudD0nMicgLz4KPC9kZWxldGVFeGVjdXRpb25zPg==
diff --git a/src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml b/src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml
new file mode 100644
index 0000000..2f89d99
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_executions_unauthorized.yaml
@@ -0,0 +1,26 @@
+!tape
+name: delete_executions_success
+interactions:
+- recorded: 2014-11-06T17:15:24.673Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/12/executions/delete
+    headers:
+      Accept: text/xml
+      Content-Type: application/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1kzmot0r2scpjfxlcpfthh7tz;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PScyJyBhbGxzdWNjZXNzZnVsPSdmYWxzZSc+CiAgPHN1Y2Nlc3NmdWwgY291bnQ9JzAnIC8+CiAgPGZhaWxlZCBjb3VudD0nMic+CiAgICA8ZXhlY3V0aW9uIGlkPSc2NDEnIG1lc3NhZ2U9J1VuYXV0aG9yaXplZDogRGVsZXRlIGV4ZWN1dGlvbiBpbiBwcm9qZWN0IHRlc3QnIC8+CiAgICA8ZXhlY3V0aW9uIGlkPSc2NDAnIG1lc3NhZ2U9J1VuYXV0aG9yaXplZDogRGVsZXRlIGV4ZWN1dGlvbiBpbiBwcm9qZWN0IHRlc3QnIC8+CiAgPC9mYWlsZWQ+CjwvZGVsZXRlRXhlY3V0aW9ucz4=

From b1c0a45da68c22d0ec8c1da134e2310039e9058f Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 09:42:34 -0800
Subject: [PATCH 61/89] Fix javadoc, require nonzero size of executionIds

---
 src/main/java/org/rundeck/api/RundeckClient.java | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 93ced66..e68dd10 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1649,17 +1649,20 @@ public class RundeckClient implements Serializable {
      * Delete a set of executions, identified by the given IDs
      *
      * @param executionIds set of identifiers for the executions - mandatory
-     * @return a {@link RundeckExecution} instance - won't be null
+     * @return a {@link DeleteExecutionsResponse} instance - won't be null
      * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
      * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
      * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
-     * @throws IllegalArgumentException if the executionId is null
+     * @throws IllegalArgumentException if the executionIds is null
      */
-    public DeleteExecutionsResponse deleteExecutions(Set executionIds)
+    public DeleteExecutionsResponse deleteExecutions(final Set executionIds)
             throws RundeckApiException, RundeckApiLoginException,
                    RundeckApiTokenException, IllegalArgumentException
     {
         AssertUtil.notNull(executionIds, "executionIds is mandatory to abort an execution !");
+        if (executionIds.size() < 1) {
+            throw new IllegalArgumentException("executionIds cannot be empty");
+        }
         final ApiPathBuilder apiPath = new ApiPathBuilder("/executions/delete").xml(
                 new DeleteExecutionsGenerator(executionIds)
         );

From e092ccdb1374df19882d1c4b892afbb0112cff3a Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 09:48:55 -0800
Subject: [PATCH 62/89] Add delete single execution

---
 .../java/org/rundeck/api/RundeckClient.java   | 17 +++++++++
 .../org/rundeck/api/RundeckClientTest.java    | 36 +++++++++++++++++++
 .../tapes/delete_execution_failure.yaml       | 21 +++++++++++
 .../tapes/delete_execution_success.yaml       | 19 ++++++++++
 4 files changed, 93 insertions(+)
 create mode 100644 src/test/resources/betamax/tapes/delete_execution_failure.yaml
 create mode 100644 src/test/resources/betamax/tapes/delete_execution_success.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index e68dd10..7811807 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1672,6 +1672,23 @@ public class RundeckClient implements Serializable {
         );
     }
 
+    /**
+     * Delete a single execution, identified by the given ID
+     *
+     * @param executionId identifier for the execution - mandatory
+     * @throws RundeckApiException in case of error when calling the API (non-existent execution with this ID)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based authentication)
+     * @throws IllegalArgumentException if the executionId is null
+     */
+    public void deleteExecution(final Long executionId)
+            throws RundeckApiException, RundeckApiLoginException,
+                   RundeckApiTokenException, IllegalArgumentException
+    {
+        AssertUtil.notNull(executionId, "executionId is mandatory to abort an execution !");
+        new ApiCall(this).delete(new ApiPathBuilder("/execution/", executionId.toString()));
+    }
+
     /*
      * History
      */
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 27fa3eb..e72aeb9 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -1572,6 +1572,42 @@ public class RundeckClientTest {
                 response.getFailures().get(1).getMessage()
         );
     }
+    /**
+     * delete single execution success
+     */
+    @Test
+    @Betamax(tape = "delete_execution_success", mode = TapeMode.READ_ONLY)
+    public void deleteExecutionSuccess() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        client.deleteExecution(643L);
+    }
+    /**
+     * delete single execution failure (does not exist)
+     */
+    @Test
+    @Betamax(tape = "delete_execution_failure", mode = TapeMode.READ_ONLY)
+    public void deleteExecutionFailure() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        try {
+            client.deleteExecution(640L);
+            Assert.fail();
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            Assert.assertEquals(404, e.getStatusCode());
+        }
+    }
+    /**
+     * delete single execution null input
+     */
+    @Test
+    public void deleteExecutionNullInput() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        try {
+            client.deleteExecution(null);
+            Assert.fail();
+        } catch (IllegalArgumentException e) {
+
+        }
+    }
     @Before
     public void setUp() throws Exception {
         // not that you can put whatever here, because we don't actually connect to the RunDeck instance
diff --git a/src/test/resources/betamax/tapes/delete_execution_failure.yaml b/src/test/resources/betamax/tapes/delete_execution_failure.yaml
new file mode 100644
index 0000000..fa26b88
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_execution_failure.yaml
@@ -0,0 +1,21 @@
+!tape
+name: delete_execution_failure
+interactions:
+- recorded: 2014-11-06T17:45:47.952Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/12/execution/640
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 404
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1eynpbisggwsy18ax352yya8k0;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    Execution ID does not exist: 640\n  \n"
diff --git a/src/test/resources/betamax/tapes/delete_execution_success.yaml b/src/test/resources/betamax/tapes/delete_execution_success.yaml
new file mode 100644
index 0000000..3c2555a
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_execution_success.yaml
@@ -0,0 +1,19 @@
+!tape
+name: delete_execution_success
+interactions:
+- recorded: 2014-11-06T17:45:47.749Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/12/execution/643
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 204
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1hlysemt7deir1j9r7l6ildg5x;Path=/

From a598c76d8eebeb8f5c91344209ab468ecd1c1bec Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 10:05:35 -0800
Subject: [PATCH 63/89] Add delete all job executions endpoint

---
 .../java/org/rundeck/api/RundeckClient.java   | 31 ++++++++++++++--
 .../org/rundeck/api/RundeckClientTest.java    | 36 +++++++++++++++++++
 .../delete_all_job_executions_success.yaml    | 23 ++++++++++++
 ...elete_all_job_executions_unauthorized.yaml | 21 +++++++++++
 4 files changed, 108 insertions(+), 3 deletions(-)
 create mode 100644 src/test/resources/betamax/tapes/delete_all_job_executions_success.yaml
 create mode 100644 src/test/resources/betamax/tapes/delete_all_job_executions_unauthorized.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 7811807..5e3e1c0 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1642,7 +1642,32 @@ public class RundeckClient implements Serializable {
         if(null!=asUser) {
             apiPath.param("asUser", asUser);
         }
-        return new ApiCall(this).get(apiPath, new AbortParser(rootXpath()+"/abort"));
+        return new ApiCall(this).get(apiPath, new AbortParser(rootXpath() + "/abort"));
+    }
+
+    /**
+     * Delete all executions for a job specified by a job ID
+     *
+     * @param jobId Identifier for the job
+     *
+     * @return a {@link DeleteExecutionsResponse} instance - won't be null
+     *
+     * @throws RundeckApiException      in case of error when calling the API (non-existent
+     *                                  execution with this ID)
+     * @throws RundeckApiLoginException if the login fails (in case of login-based authentication)
+     * @throws RundeckApiTokenException if the token is invalid (in case of token-based
+     *                                  authentication)
+     * @throws IllegalArgumentException if the executionIds is null
+     */
+    public DeleteExecutionsResponse deleteAllJobExecutions(final String jobId)
+            throws RundeckApiException, RundeckApiLoginException,
+                   RundeckApiTokenException, IllegalArgumentException
+    {
+        AssertUtil.notNull(jobId, "jobId is mandatory to delete executions!");
+        return new ApiCall(this).delete(
+                new ApiPathBuilder("/job/",jobId,"/executions"),
+                new DeleteExecutionsResponseParser(rootXpath() + "/deleteExecutions")
+        );
     }
 
     /**
@@ -1659,7 +1684,7 @@ public class RundeckClient implements Serializable {
             throws RundeckApiException, RundeckApiLoginException,
                    RundeckApiTokenException, IllegalArgumentException
     {
-        AssertUtil.notNull(executionIds, "executionIds is mandatory to abort an execution !");
+        AssertUtil.notNull(executionIds, "executionIds is mandatory to delete executions!");
         if (executionIds.size() < 1) {
             throw new IllegalArgumentException("executionIds cannot be empty");
         }
@@ -1685,7 +1710,7 @@ public class RundeckClient implements Serializable {
             throws RundeckApiException, RundeckApiLoginException,
                    RundeckApiTokenException, IllegalArgumentException
     {
-        AssertUtil.notNull(executionId, "executionId is mandatory to abort an execution !");
+        AssertUtil.notNull(executionId, "executionId is mandatory to delete an execution!");
         new ApiCall(this).delete(new ApiPathBuilder("/execution/", executionId.toString()));
     }
 
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index e72aeb9..b4f5ff4 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -1572,6 +1572,42 @@ public class RundeckClientTest {
                 response.getFailures().get(1).getMessage()
         );
     }
+    /**
+     * delete executions with failure
+     */
+    @Test
+    @Betamax(tape = "delete_all_job_executions_unauthorized", mode = TapeMode.READ_ONLY)
+    public void deleteAllJobExecutionsUnauthorized() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        try {
+            final DeleteExecutionsResponse response = client.deleteAllJobExecutions(
+                    "764c1209-68ed-4185-8d43-a739364bf156"
+            );
+            Assert.fail();
+        } catch (RundeckApiException.RundeckApiTokenException e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     *
+     * @throws Exception
+     */
+    @Test
+    @Betamax(tape = "delete_all_job_executions_success", mode = TapeMode.READ_ONLY)
+    public void deleteAllJobExecutionsSuccess() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_8, 12);
+        final DeleteExecutionsResponse response = client.deleteAllJobExecutions(
+                "764c1209-68ed-4185-8d43-a739364bf156"
+        );
+        Assert.assertEquals(2, response.getRequestCount());
+        Assert.assertEquals(2, response.getSuccessCount());
+        Assert.assertEquals(0, response.getFailedCount());
+        Assert.assertTrue(response.isAllsuccessful());
+        Assert.assertNotNull(response.getFailures());
+        Assert.assertEquals(0, response.getFailures().size());
+    }
+
     /**
      * delete single execution success
      */
diff --git a/src/test/resources/betamax/tapes/delete_all_job_executions_success.yaml b/src/test/resources/betamax/tapes/delete_all_job_executions_success.yaml
new file mode 100644
index 0000000..b68b5e2
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_all_job_executions_success.yaml
@@ -0,0 +1,23 @@
+!tape
+name: delete_all_job_executions_success
+interactions:
+- recorded: 2014-11-06T18:03:44.789Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/12/job/764c1209-68ed-4185-8d43-a739364bf156/executions
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=cqqoff205qus10sz1v3t54rk7;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGRlbGV0ZUV4ZWN1dGlvbnMgcmVxdWVzdENvdW50PScyJyBhbGxzdWNjZXNzZnVsPSd0cnVlJz4KICA8c3VjY2Vzc2Z1bCBjb3VudD0nMicgLz4KPC9kZWxldGVFeGVjdXRpb25zPg==
diff --git a/src/test/resources/betamax/tapes/delete_all_job_executions_unauthorized.yaml b/src/test/resources/betamax/tapes/delete_all_job_executions_unauthorized.yaml
new file mode 100644
index 0000000..f4f898b
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_all_job_executions_unauthorized.yaml
@@ -0,0 +1,21 @@
+!tape
+name: delete_all_job_executions_unauthorized
+interactions:
+- recorded: 2014-11-06T17:57:02.905Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/12/job/764c1209-68ed-4185-8d43-a739364bf156/executions
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 12
+      X-RunDeck-Auth-Token: GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI
+  response:
+    status: 403
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=splzhkd5xunl1noidus2o7im3;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    Not authorized for action \"Delete Execution\" for Project test\n  \n"

From 40f5c5b891b8256d41b33562337b48035c97bdac Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 10:06:07 -0800
Subject: [PATCH 64/89] Update API v12 todo

---
 src/site/confluence/status.confluence | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index c161cf4..343131c 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -118,6 +118,6 @@ h2. Rundeck API version 12
 
 [Documentation of the RunDeck API version 12|http://rundeck.org/2.2.0/api/index.html]
 
-* Bulk delete executions - TBD
-* Delete execution - TBD
-* Delete all executions for a job - TBD
+* Bulk delete executions - OK
+* Delete execution - OK
+* Delete all executions for a job - OK

From e2839d6244777115e608fc019d497d2b99f42266 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 10:24:51 -0800
Subject: [PATCH 65/89] Expect 204 response for job delete for #14

---
 .../java/org/rundeck/api/RundeckClient.java   |  3 ++-
 .../org/rundeck/api/RundeckClientTest.java    | 26 +++++++++++++++++++
 .../resources/betamax/tapes/delete_job.yaml   | 19 ++++++++++++++
 .../betamax/tapes/delete_job_not_found.yaml   | 21 +++++++++++++++
 4 files changed, 68 insertions(+), 1 deletion(-)
 create mode 100644 src/test/resources/betamax/tapes/delete_job.yaml
 create mode 100644 src/test/resources/betamax/tapes/delete_job_not_found.yaml

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 8462dba..bc23457 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1039,7 +1039,8 @@ public class RundeckClient implements Serializable {
     public String deleteJob(String jobId) throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException, IllegalArgumentException {
         AssertUtil.notBlank(jobId, "jobId is mandatory to delete a job !");
-        return new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId), new StringParser(rootXpath()+"/success/message"));
+        new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId));
+        return "Job " + jobId + " was deleted successfully";
     }
     /**
      * Delete multiple jobs, identified by the given IDs
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index fb01b80..5dc6d59 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -1489,6 +1489,32 @@ public class RundeckClientTest {
             Assert.assertEquals(404,e.getStatusCode());
         }
     }
+    /**
+     * delete job
+     */
+    @Test
+    @Betamax(tape = "delete_job", mode = TapeMode.READ_ONLY)
+    public void deleteJob() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        String result = client.deleteJob("api-test-job-run-scheduled");
+
+        Assert.assertEquals("Job api-test-job-run-scheduled was deleted successfully", result);
+    }
+    /**
+     * delete job (DNE)
+     */
+    @Test
+    @Betamax(tape = "delete_job_not_found", mode = TapeMode.READ_ONLY)
+    public void deleteJobNotFound() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        try {
+            String result = client.deleteJob("does-not-exist");
+            Assert.fail();
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            Assert.assertEquals(404, e.getStatusCode());
+        }
+
+    }
 
     @Before
     public void setUp() throws Exception {
diff --git a/src/test/resources/betamax/tapes/delete_job.yaml b/src/test/resources/betamax/tapes/delete_job.yaml
new file mode 100644
index 0000000..29e33df
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_job.yaml
@@ -0,0 +1,19 @@
+!tape
+name: delete_job
+interactions:
+- recorded: 2014-11-06T18:14:48.773Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/11/job/api-test-job-run-scheduled
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 204
+    headers:
+      Content-Type: text/html;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1hb33nx0a7qv21gyuoqes2pahg;Path=/
diff --git a/src/test/resources/betamax/tapes/delete_job_not_found.yaml b/src/test/resources/betamax/tapes/delete_job_not_found.yaml
new file mode 100644
index 0000000..f7540a6
--- /dev/null
+++ b/src/test/resources/betamax/tapes/delete_job_not_found.yaml
@@ -0,0 +1,21 @@
+!tape
+name: delete_job_not_found
+interactions:
+- recorded: 2014-11-06T18:21:13.147Z
+  request:
+    method: DELETE
+    uri: http://rundeck.local:4440/api/11/job/does-not-exist
+    headers:
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 404
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1sxop3bupsrqb1gupbru3vklba;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    Job ID does not exist: does-not-exist\n  \n"

From e73bb27058c3e7f55d8b7c06e19f3292fe0d95d1 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 10:33:59 -0800
Subject: [PATCH 66/89] Release info

---
 src/changes/changes.xml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 825c4ca..6f25616 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -22,6 +22,11 @@
     Vincent Behar
   
   
+      
+          
+              Issue #14: deleteJob fails
+          
+      
       
           Project creation
           Get Project configuration

From 7228205e57064ca124d546d6be7ec7df7aa0c710 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 10:35:13 -0800
Subject: [PATCH 67/89] [maven-release-plugin] prepare release
 rundeck-api-java-client-11.1

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 722956b..a7b9d1e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  11.1-SNAPSHOT
+  11.1
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From d7928bc37e259f96d2d5dfb416c738f826facaf8 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Thu, 6 Nov 2014 10:35:17 -0800
Subject: [PATCH 68/89] [maven-release-plugin] prepare for next development
 iteration

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index a7b9d1e..7d506cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,7 +27,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  11.1
+  11.2-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From 75231805ecd1981ef4797846a4c41e207b31a4e7 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Nov 2014 15:37:19 -0800
Subject: [PATCH 69/89] Test query parameter generation for date strings

---
 .../api/ExecutionQueryParametersTest.java     | 54 +++++++++++++++++++
 1 file changed, 54 insertions(+)
 create mode 100644 src/test/java/org/rundeck/api/ExecutionQueryParametersTest.java

diff --git a/src/test/java/org/rundeck/api/ExecutionQueryParametersTest.java b/src/test/java/org/rundeck/api/ExecutionQueryParametersTest.java
new file mode 100644
index 0000000..dad5254
--- /dev/null
+++ b/src/test/java/org/rundeck/api/ExecutionQueryParametersTest.java
@@ -0,0 +1,54 @@
+package org.rundeck.api;
+
+import junit.framework.Assert;
+import org.junit.Test;
+import org.rundeck.api.query.ExecutionQuery;
+
+import java.util.Arrays;
+import java.util.Date;
+
+/**
+ * ExecutionQueryParametersTest is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-11-07
+ */
+public class ExecutionQueryParametersTest {
+
+    @Test
+    public void stringParameter() {
+        ExecutionQuery.Builder description = ExecutionQuery.builder().description("a description");
+        ExecutionQueryParameters executionQueryParameters = new ExecutionQueryParameters(
+                description.build()
+        );
+        ApiPathBuilder param = new ApiPathBuilder("").param(executionQueryParameters);
+        Assert.assertEquals("?descFilter=a+description", param.toString());
+    }
+    @Test
+    public void listParameter() {
+        ExecutionQuery.Builder description = ExecutionQuery.builder().excludeJobList(
+                Arrays.asList(
+                        "a",
+                        "b"
+                )
+        );
+        ExecutionQueryParameters executionQueryParameters = new ExecutionQueryParameters(
+                description.build()
+        );
+        ApiPathBuilder param = new ApiPathBuilder("").param(executionQueryParameters);
+        Assert.assertEquals("?excludeJobListFilter=a&excludeJobListFilter=b", param.toString());
+    }
+    @Test
+    public void dateParameter() {
+        ExecutionQuery.Builder description = ExecutionQuery.builder().end(
+                new Date(1347581178168L)
+        );
+        ExecutionQueryParameters executionQueryParameters = new ExecutionQueryParameters(
+                description.build()
+        );
+        ApiPathBuilder param = new ApiPathBuilder("").param(executionQueryParameters);
+        //nb: timezone should be GMT
+        //2012-09-14T00:06:18Z
+        Assert.assertEquals("?end=2012-09-14T00%3A06%3A18Z", param.toString());
+    }
+}

From 3219161dd34a23bcbfc7cb355e54e536e4b4544c Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Nov 2014 15:42:22 -0800
Subject: [PATCH 70/89] Use GMT time zone for date formatter

---
 src/main/java/org/rundeck/api/QueryParameterBuilder.java | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/main/java/org/rundeck/api/QueryParameterBuilder.java b/src/main/java/org/rundeck/api/QueryParameterBuilder.java
index 6fd0126..23db9bb 100644
--- a/src/main/java/org/rundeck/api/QueryParameterBuilder.java
+++ b/src/main/java/org/rundeck/api/QueryParameterBuilder.java
@@ -36,6 +36,9 @@ import java.util.*;
 abstract class QueryParameterBuilder implements ApiPathBuilder.BuildsParameters {
     public static final String W3C_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
     static final SimpleDateFormat format = new SimpleDateFormat(W3C_DATE_FORMAT);
+    static {
+        format.setTimeZone(TimeZone.getTimeZone("GMT"));
+    }
 
     /**
      * Add a value to the builder for a given key

From 7bad157911feb120e57d8bf5d542ba34f8726efe Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Nov 2014 15:42:37 -0800
Subject: [PATCH 71/89] Fix expected generated dates for executions test

---
 src/test/resources/betamax/tapes/get_executions.yaml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/resources/betamax/tapes/get_executions.yaml b/src/test/resources/betamax/tapes/get_executions.yaml
index 740a0c5..f87fb2d 100644
--- a/src/test/resources/betamax/tapes/get_executions.yaml
+++ b/src/test/resources/betamax/tapes/get_executions.yaml
@@ -38,7 +38,7 @@ interactions:
 - recorded: 2012-09-14T22:04:13.342Z
   request:
     method: GET
-    uri: http://rundeck.local:4440/api/5/executions?project=blah&begin=2012-09-13T17%3A06%3A18Z&max=2&offset=0
+    uri: http://rundeck.local:4440/api/5/executions?project=blah&begin=2012-09-14T00%3A06%3A18Z&max=2&offset=0
     headers:
       Accept: text/xml
       Host: rundeck.local:4440
@@ -54,7 +54,7 @@ interactions:
 - recorded: 2012-09-14T22:04:13.404Z
   request:
     method: GET
-    uri: http://rundeck.local:4440/api/5/executions?project=blah&end=2012-09-13T17%3A06%3A18Z&max=2&offset=0
+    uri: http://rundeck.local:4440/api/5/executions?project=blah&end=2012-09-14T00%3A06%3A18Z&max=2&offset=0
     headers:
       Accept: text/xml
       Host: rundeck.local:4440

From 18ec800264d9fcaefced41329c5be973f36cca1e Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Nov 2014 11:12:02 -0800
Subject: [PATCH 72/89] Test expected API v11 response

---
 .../rundeck/api/parser/PagedResultParser.java |  11 +-
 .../org/rundeck/api/RundeckClientTest.java    | 126 ++++++
 .../betamax/tapes/get_executions_v11.yaml     | 419 ++++++++++++++++++
 3 files changed, 554 insertions(+), 2 deletions(-)
 create mode 100644 src/test/resources/betamax/tapes/get_executions_v11.yaml

diff --git a/src/main/java/org/rundeck/api/parser/PagedResultParser.java b/src/main/java/org/rundeck/api/parser/PagedResultParser.java
index 3eed01c..3389a87 100644
--- a/src/main/java/org/rundeck/api/parser/PagedResultParser.java
+++ b/src/main/java/org/rundeck/api/parser/PagedResultParser.java
@@ -26,6 +26,7 @@ package org.rundeck.api.parser;
 
 import org.dom4j.Element;
 import org.dom4j.Node;
+import org.rundeck.api.RundeckApiException;
 import org.rundeck.api.util.PagedResults;
 
 import java.util.*;
@@ -38,7 +39,7 @@ import java.util.*;
  */
 public class PagedResultParser implements XmlNodeParser> {
     ListParser itemParser;
-    String xpath;
+    private String xpath;
 
     /**
      * Create a PagedResultParser
@@ -54,7 +55,9 @@ public class PagedResultParser implements XmlNodeParser> {
     @Override
     public PagedResults parseXmlNode(Node node) {
         Node pagedNodeContainer = node.selectSingleNode(xpath);
-
+        if(null==pagedNodeContainer) {
+            throw new RundeckApiException("XML content did not match XPATH expression: " + xpath);
+        }
         Element el = (Element) pagedNodeContainer;
         final int max = integerAttribute(el, "max", -1);
         final int offset = integerAttribute(el, "offset", -1);
@@ -113,4 +116,8 @@ public class PagedResultParser implements XmlNodeParser> {
         }
         return parseMax;
     }
+
+    public String getXpath() {
+        return xpath;
+    }
 }
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 21f306f..d4d844e 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -402,6 +402,132 @@ public class RundeckClientTest {
                                                                            .build(), 2L, 0L);
         assertPageResults(adhocTest, 2, 2, 2, 0, 2);
     }
+    @Test
+    @Betamax(tape = "get_executions_v11",
+             mode = TapeMode.READ_ONLY,
+             match = {MatchRule.uri, MatchRule.headers, MatchRule.method, MatchRule.path, MatchRule.query})
+    public void getExecutionsV11() throws Exception {
+
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+
+        final String projectName = "blah";
+        final PagedResults jobTest = client.getExecutions(ExecutionQuery.builder()
+                                                                        .project(projectName)
+                                                                        .job("test job")
+                                                                        .build(),
+                                                                    2L,
+                                                                    0L);
+        assertPageResults(jobTest, 2, 2, 2, 0, 2);
+        final PagedResults jobExactTest = client.getExecutions(ExecutionQuery.builder()
+                                                                             .project(projectName)
+                                                                             .jobExact("test job")
+                                                                             .build(),
+                                                                         2L,
+                                                                         0L);
+        assertPageResults(jobExactTest, 2, 2, 2, 0, 2);
+        final PagedResults excludeJobTest = client.getExecutions(ExecutionQuery.builder()
+                                                                        .project(projectName)
+                                                                        .excludeJob("test job")
+                                                                        .build(),
+                                                                    2L,
+                                                                    0L);
+        assertPageResults(excludeJobTest, 2, 2, 2, 0, 2);
+        final PagedResults excludeJobExactTest = client.getExecutions(ExecutionQuery.builder()
+                                                                             .project(projectName)
+                                                                             .excludeJobExact("test job")
+                                                                             .build(),
+                                                                         2L,
+                                                                         0L);
+        assertPageResults(excludeJobExactTest, 2, 2, 2, 0, 2);
+        final PagedResults descriptionTest = client.getExecutions(ExecutionQuery.builder()
+                                                                                .project(projectName)
+                                                                                .description("a description")
+                                                                                .build(), 2L, 0L);
+        assertPageResults(descriptionTest, 2, 2, 2, 0, 2);
+        final PagedResults abortedbyTest = client.getExecutions(ExecutionQuery.builder()
+                                                                              .project(projectName)
+                                                                              .abortedby("admin")
+                                                                              .build(),
+                                                                          2L,
+                                                                          0L);
+        assertPageResults(abortedbyTest, 1, 1, 2, 0, 1);
+        final PagedResults beginTest = client.getExecutions(ExecutionQuery.builder()
+                                                                          .project(projectName)
+                                                                          .begin(new Date(1347581178168L))
+                                                                          .build(), 2L, 0L);
+        assertPageResults(beginTest, 2, 2, 2, 0, 6);
+        final PagedResults endTest = client.getExecutions(ExecutionQuery.builder()
+                                                                        .project(projectName)
+                                                                        .end(new Date(1415388156385L))
+                                                                        .build(), 2L, 0L);
+        assertPageResults(endTest, 2, 2, 2, 0, 4);
+        final List excludeJobIdList = Arrays.asList("123", "456");
+        final PagedResults excludeJobIdListTest = client.getExecutions(ExecutionQuery.builder()
+                                                                                     .project(projectName)
+                                                                                     .excludeJobIdList(excludeJobIdList)
+                                                                                     .build(), 2L, 0L);
+        assertPageResults(excludeJobIdListTest, 2, 2, 2, 0, 4);
+        final List jobList = Arrays.asList("fruit/mango", "fruit/lemon");
+        final PagedResults jobListTest = client.getExecutions(ExecutionQuery.builder()
+                                                                            .project(projectName)
+                                                                            .jobList(jobList)
+                                                                            .build(), 2L, 0L);
+        assertPageResults(jobListTest, 2, 2, 2, 0, 2);
+        final List excludeJobList = Arrays.asList("a/path/job1", "path/to/job2");
+        final PagedResults excludeJobListTest = client.getExecutions(ExecutionQuery.builder()
+                                                                                   .project(projectName)
+                                                                                   .excludeJobList(excludeJobList)
+                                                                                   .build(), 2L, 0L);
+        assertPageResults(excludeJobListTest, 2, 2, 2, 0, 4);
+        final List list = Arrays.asList("9aa33253-17a3-4dce-890c-e5f10f9f00d6",
+                                                "2dd94199-00c4-4690-9b4d-beda4812bed0");
+        final PagedResults jobIdListTest = client.getExecutions(ExecutionQuery.builder()
+                                                                              .project(projectName)
+                                                                              .jobIdList(list)
+                                                                              .build(), 2L, 0L);
+        assertPageResults(jobIdListTest, 2, 2, 2, 0, 2);
+        final PagedResults groupPathTest = client.getExecutions(ExecutionQuery.builder()
+                                                                              .project(projectName)
+                                                                              .groupPath("fruit")
+                                                                              .build(),
+                                                                          2L,
+                                                                          0L);
+        assertPageResults(groupPathTest, 2, 2, 2, 0, 2);
+        final PagedResults groupPathExactTest = client.getExecutions(ExecutionQuery.builder()
+                                                                                   .project(projectName)
+                                                                                   .groupPathExact("fruit")
+                                                                                   .build(), 2L, 0L);
+        assertPageResults(groupPathExactTest, 2, 2, 2, 0, 2);
+
+        final PagedResults excludeGroupPathTest = client.getExecutions(ExecutionQuery.builder()
+                                                                              .project(projectName)
+                                                                              .excludeGroupPath("fruit")
+                                                                              .build(),
+                                                                          2L,
+                                                                          0L);
+        assertPageResults(excludeGroupPathTest, 2, 2, 2, 0, 2);
+        final PagedResults excliudeGroupPathExactTest = client.getExecutions(ExecutionQuery.builder()
+                                                                                   .project(projectName)
+                                                                                   .excludeGroupPathExact("fruit")
+                                                                                   .build(), 2L, 0L);
+        assertPageResults(excliudeGroupPathExactTest, 2, 2, 2, 0, 2);
+
+        final PagedResults recentTest = client.getExecutions(ExecutionQuery.builder()
+                                                                           .project(projectName)
+                                                                           .recent("1h").build(), 2L, 0L);
+        assertPageResults(recentTest, 2, 2, 2, 0, 2);
+        final PagedResults statusTest = client.getExecutions(ExecutionQuery.builder()
+                                                                           .project(projectName)
+                                                                           .status(RundeckExecution.ExecutionStatus.SUCCEEDED)
+                                                                           .build(), 2L, 0L);
+        assertPageResults(statusTest, 2, 2, 2, 0, 3);
+        final PagedResults adhocTest = client.getExecutions(ExecutionQuery.builder()
+                                                                           .project(projectName)
+                                                                           .adhoc(true)
+                                                                           .build(), 2L, 0L);
+        assertPageResults(adhocTest, 2, 2, 2, 0, 2);
+    }
 
     /**
      * Test paging values from results
diff --git a/src/test/resources/betamax/tapes/get_executions_v11.yaml b/src/test/resources/betamax/tapes/get_executions_v11.yaml
new file mode 100644
index 0000000..ae592b0
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_executions_v11.yaml
@@ -0,0 +1,419 @@
+!tape
+name: get_executions_v11
+interactions:
+- recorded: 2014-11-07T23:18:13.076Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?jobFilter=test+job&project=blah&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1jt4f1ctz8i6rfsou34u5suz2;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:51:44Z\n      2014-11-07T18:51:49Z\n      \n        test job\n\
+      \        \n        blah\n        a description\n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:51:32Z\n      2014-11-07T18:51:39Z\n\
+      \      admin\n      \n        test job\n        \n        blah\n        a description\n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:18:13.426Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?jobExactFilter=test+job&project=blah&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:51:44Z\n      2014-11-07T18:51:49Z\n      \n        test job\n\
+      \        \n        blah\n        a description\n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:51:32Z\n      2014-11-07T18:51:39Z\n\
+      \      admin\n      \n        test job\n        \n        blah\n        a description\n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:18:13.591Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?excludeJobFilter=test+job&project=blah&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:18:13.748Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?excludeJobExactFilter=test+job&project=blah&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:18:13.897Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&descFilter=a+description&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:51:44Z\n      2014-11-07T18:51:49Z\n      \n        test job\n\
+      \        \n        blah\n        a description\n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:51:32Z\n      2014-11-07T18:51:39Z\n\
+      \      admin\n      \n        test job\n        \n        blah\n        a description\n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:18:14.025Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&abortedbyFilter=admin&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:51:32Z\n      2014-11-07T18:51:39Z\n      admin\n      \n\
+      \        test job\n        \n        blah\n        a description\n      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:18:14.155Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&begin=2012-09-14T00%3A06%3A18Z&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T23:17:55Z\n\
+      \      2014-11-07T23:17:56Z\n      echo bye ; false\n      \n      \n        \n      \n    \n    \n      admin\n      2014-11-07T23:17:47Z\n      2014-11-07T23:17:50Z\n\
+      \      echo hi ; false\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:50.560Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&end=2014-11-07T19%3A22%3A36Z&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1olgmevldd8n2d4lib3pzifep;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:50.701Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&excludeJobIdListFilter=123&excludeJobIdListFilter=456&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:50.837Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&jobListFilter=fruit%2Fmango&jobListFilter=fruit%2Flemon&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:50.966Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&excludeJobListFilter=a%2Fpath%2Fjob1&excludeJobListFilter=path%2Fto%2Fjob2&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.097Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&jobIdListFilter=9aa33253-17a3-4dce-890c-e5f10f9f00d6&jobIdListFilter=2dd94199-00c4-4690-9b4d-beda4812bed0&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.229Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&groupPath=fruit&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.358Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&groupPathExact=fruit&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.483Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&excludeGroupPath=fruit&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:51:44Z\n      2014-11-07T18:51:49Z\n      \n        test job\n\
+      \        \n        blah\n        a description\n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:51:32Z\n      2014-11-07T18:51:39Z\n\
+      \      admin\n      \n        test job\n        \n        blah\n        a description\n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.607Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&excludeGroupPathExact=fruit&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:51:44Z\n      2014-11-07T18:51:49Z\n      \n        test job\n\
+      \        \n        blah\n        a description\n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:51:32Z\n      2014-11-07T18:51:39Z\n\
+      \      admin\n      \n        test job\n        \n        blah\n        a description\n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.724Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&recentFilter=1h&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T23:17:55Z\n\
+      \      2014-11-07T23:17:56Z\n      echo bye ; false\n      \n      \n        \n      \n    \n    \n      admin\n      2014-11-07T23:17:47Z\n      2014-11-07T23:17:50Z\n\
+      \      echo hi ; false\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:19:51.845Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&statusFilter=succeeded&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T18:54:10Z\n      2014-11-07T18:54:16Z\n      \n        mango\n   \
+      \     fruit\n        blah\n        \n      \n      sleep 5\n      \n      \n        \n      \n\
+      \    \n    \n      admin\n      2014-11-07T18:54:08Z\n      2014-11-07T18:54:14Z\n      \n        lemon\n        fruit\n        blah\n        \n\
+      \      \n      sleep 5\n      \n      \n        \n      \n    \n  \n"
+- recorded: 2014-11-07T23:20:12.975Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/executions?project=blah&adhoc=true&max=2&offset=0
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=11ubcx5ejb1sr6nycorkmdrg3;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-07T23:17:55Z\n\
+      \      2014-11-07T23:17:56Z\n      echo bye ; false\n      \n      \n        \n      \n    \n    \n      admin\n      2014-11-07T23:17:47Z\n      2014-11-07T23:17:50Z\n\
+      \      echo hi ; false\n      \n      \n        \n      \n    \n  \n"

From c8e1315612ab873f075a6b63ec46838636b08bb5 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Fri, 7 Nov 2014 15:23:12 -0800
Subject: [PATCH 73/89] Patch rundeck API v11 bug for execution query results

---
 .../java/org/rundeck/api/RundeckClient.java   | 19 ++++++++--
 .../parser/PagedResultParser_BugPatchV11.java | 35 +++++++++++++++++++
 2 files changed, 51 insertions(+), 3 deletions(-)
 create mode 100644 src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 8ada6d9..101bf6d 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1587,13 +1587,26 @@ public class RundeckClient implements Serializable {
                                          .param(new ExecutionQueryParameters(query))
                                          .param("max", max)
                                          .param("offset", offset),
-                                     new PagedResultParser(
-                                         new ListParser(new ExecutionParser(), "execution"),
-                                         rootXpath()+"/executions"
+                                     patchApiV11Response(
+                                             new PagedResultParser(
+                                                     new ListParser(new ExecutionParser(), "execution"),
+                                                     rootXpath() + "/executions"
+                                             )
                                      )
         );
     }
 
+    /**
+     * Fix potential buggy response from Rundeck, where <result> wrapper exists
+     * even if it should not.
+     * @param parser
+     * @return
+     */
+    private XmlNodeParser> patchApiV11Response
+            (final PagedResultParser parser) {
+        return new PagedResultParser_BugPatchV11(parser);
+    }
+
     /**
      * Get a single execution, identified by the given ID
      *
diff --git a/src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java b/src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java
new file mode 100644
index 0000000..083b3d1
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java
@@ -0,0 +1,35 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Node;
+import org.rundeck.api.util.PagedResults;
+
+/**
+ * WRaps a {@link PagedResultParser} to detect whether the result XML incorrectly is wrapped with a
+ * <result> element.
+ *
+ * @author Greg Schueler 
+ * @since 2014-11-07
+ */
+public class PagedResultParser_BugPatchV11 implements XmlNodeParser> {
+    PagedResultParser parser;
+
+    public PagedResultParser_BugPatchV11(final PagedResultParser parser) {
+        this.parser = parser;
+    }
+
+    @Override public PagedResults parseXmlNode(final Node node) {
+        Node sourceNode = node;
+        final Node tested = sourceNode.selectSingleNode(parser.getXpath());
+        if (null == tested && !parser.getXpath().startsWith("result")) {
+            //prepend /result
+            if (null != sourceNode.selectSingleNode("result" + parser.getXpath())) {
+                sourceNode = sourceNode.selectSingleNode("result" + parser.getXpath());
+                sourceNode.getParent().remove(sourceNode);
+                DocumentHelper.createDocument((Element) sourceNode);
+            }
+        }
+        return parser.parseXmlNode(sourceNode);
+    }
+}

From 8cc48ecd89952080fba13be6020f65f662c69898 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Mon, 10 Nov 2014 11:14:27 -0800
Subject: [PATCH 74/89] Test api v11 response for trigger job

---
 .../org/rundeck/api/RundeckClientTest.java    | 37 +++++++++++++++++++
 .../betamax/tapes/trigger_job_basic_v11.yaml  | 24 ++++++++++++
 .../tapes/trigger_job_basic_v11_patch.yaml    | 24 ++++++++++++
 3 files changed, 85 insertions(+)
 create mode 100644 src/test/resources/betamax/tapes/trigger_job_basic_v11.yaml
 create mode 100644 src/test/resources/betamax/tapes/trigger_job_basic_v11_patch.yaml

diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index d4d844e..97b743d 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -634,6 +634,43 @@ public class RundeckClientTest {
         Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
 
     }
+    @Test
+    @Betamax(tape = "trigger_job_basic_v11")
+    public void triggerJobBasic_v11() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+        final RundeckExecution test
+            = client.triggerJob(RunJobBuilder.builder().setJobId("bda8b956-43a5-4eef-9c67" +
+                                                                 "-3f27cc0ee1a5").build());
+
+        Assert.assertEquals((Long) 943L, test.getId());
+        Assert.assertEquals(null, test.getArgstring());
+        Assert.assertEquals(null, test.getAbortedBy());
+        Assert.assertEquals("echo hi there ${job.username} ; sleep 90", test.getDescription());
+        Assert.assertEquals("admin", test.getStartedBy());
+        Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
+    }
+
+    /**
+     * Response for API v11 incorrectly includes <result>, but we should handle this case
+     * @throws Exception
+     */
+    @Test
+    @Betamax(tape = "trigger_job_basic_v11_patch")
+    public void triggerJobBasic_v11_patch() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+        final RundeckExecution test
+            = client.triggerJob(RunJobBuilder.builder().setJobId("bda8b956-43a5-4eef-9c67" +
+                                                                 "-3f27cc0ee1a5").build());
+
+        Assert.assertEquals((Long) 944L, test.getId());
+        Assert.assertEquals(null, test.getArgstring());
+        Assert.assertEquals(null, test.getAbortedBy());
+        Assert.assertEquals("echo hi there ${job.username} ; sleep 90", test.getDescription());
+        Assert.assertEquals("admin", test.getStartedBy());
+        Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
+    }
 
     @Test
     @Betamax(tape = "trigger_job_as_user")
diff --git a/src/test/resources/betamax/tapes/trigger_job_basic_v11.yaml b/src/test/resources/betamax/tapes/trigger_job_basic_v11.yaml
new file mode 100644
index 0000000..d6539c5
--- /dev/null
+++ b/src/test/resources/betamax/tapes/trigger_job_basic_v11.yaml
@@ -0,0 +1,24 @@
+!tape
+name: trigger_job_basic_v11
+interactions:
+- recorded: 2014-11-08T00:00:58.749Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/job/bda8b956-43a5-4eef-9c67-3f27cc0ee1a5/run
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=19ui1v6otua3h1714jv2zbkbid;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbnMgY291bnQ9JzEnPgogIDxleGVjdXRpb24gaWQ9Jzk0MycgaHJlZj0naHR0cDovL2RpZ25hbjo0NDQwL2V4ZWN1dGlvbi9mb2xsb3cvOTQzJyBzdGF0dXM9J3J1bm5pbmcnIHByb2plY3Q9J2RlbW8nPgogICAgPHVzZXI+YWRtaW48L3VzZXI+CiAgICA8ZGF0ZS1zdGFydGVkIHVuaXh0aW1lPScxNDE1NDA0ODU4NTE4Jz4yMDE0LTExLTA4VDAwOjAwOjU4WjwvZGF0ZS1zdGFydGVkPgogICAgPGpvYiBpZD0nYmRhOGI5NTYtNDNhNS00ZWVmLTljNjctM2YyN2NjMGVlMWE1Jz4KICAgICAgPG5hbWU+YSBqb2I8L25hbWU+CiAgICAgIDxncm91cD48L2dyb3VwPgogICAgICA8cHJvamVjdD5kZW1vPC9wcm9qZWN0PgogICAgICA8ZGVzY3JpcHRpb24+PC9kZXNjcmlwdGlvbj4KICAgIDwvam9iPgogICAgPGRlc2NyaXB0aW9uPmVjaG8gaGkgdGhlcmUgJHtqb2IudXNlcm5hbWV9IDsgc2xlZXAgOTA8L2Rlc2NyaXB0aW9uPgogICAgPGFyZ3N0cmluZyAvPgogIDwvZXhlY3V0aW9uPgo8L2V4ZWN1dGlvbnM+
diff --git a/src/test/resources/betamax/tapes/trigger_job_basic_v11_patch.yaml b/src/test/resources/betamax/tapes/trigger_job_basic_v11_patch.yaml
new file mode 100644
index 0000000..7731908
--- /dev/null
+++ b/src/test/resources/betamax/tapes/trigger_job_basic_v11_patch.yaml
@@ -0,0 +1,24 @@
+!tape
+name: trigger_job_basic_v11_patch
+interactions:
+- recorded: 2014-11-08T00:05:15.235Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/job/bda8b956-43a5-4eef-9c67-3f27cc0ee1a5/run
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1dfooymrwshimhr8fe4ncoc93;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-08T00:05:14Z\n\
+      \      \n        a job\n        \n        demo\n        \n      \n      echo hi there ${job.username}\
+      \ ; sleep 90\n      \n    \n  \n"

From bceb1ec4c6c862d3e08ecb0f6c99b380af2923f0 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Mon, 10 Nov 2014 11:15:23 -0800
Subject: [PATCH 75/89] Refactor apiv11 result unwrap to utility class

---
 .../java/org/rundeck/api/RundeckClient.java   | 21 ++---
 .../org/rundeck/api/parser/APIV11Helper.java  | 78 +++++++++++++++++++
 .../parser/PagedResultParser_BugPatchV11.java | 35 ---------
 3 files changed, 85 insertions(+), 49 deletions(-)
 create mode 100644 src/main/java/org/rundeck/api/parser/APIV11Helper.java
 delete mode 100644 src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 101bf6d..a610219 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1587,26 +1587,19 @@ public class RundeckClient implements Serializable {
                                          .param(new ExecutionQueryParameters(query))
                                          .param("max", max)
                                          .param("offset", offset),
-                                     patchApiV11Response(
+                                     APIV11Helper.unwrapIfNeeded(
                                              new PagedResultParser(
-                                                     new ListParser(new ExecutionParser(), "execution"),
+                                                     new ListParser(
+                                                             new ExecutionParser(),
+                                                             "execution"
+                                                     ),
                                                      rootXpath() + "/executions"
-                                             )
+                                             ),
+                                             rootXpath() + "/executions"
                                      )
         );
     }
 
-    /**
-     * Fix potential buggy response from Rundeck, where <result> wrapper exists
-     * even if it should not.
-     * @param parser
-     * @return
-     */
-    private XmlNodeParser> patchApiV11Response
-            (final PagedResultParser parser) {
-        return new PagedResultParser_BugPatchV11(parser);
-    }
-
     /**
      * Get a single execution, identified by the given ID
      *
diff --git a/src/main/java/org/rundeck/api/parser/APIV11Helper.java b/src/main/java/org/rundeck/api/parser/APIV11Helper.java
new file mode 100644
index 0000000..a9711b2
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/APIV11Helper.java
@@ -0,0 +1,78 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.DocumentHelper;
+import org.dom4j.Element;
+import org.dom4j.Node;
+
+/**
+ * Utility to handle API v11 responses with <result> wrapper element.
+ *
+ * @author Greg Schueler 
+ * @since 2014-11-10
+ */
+public class APIV11Helper {
+
+    /**
+     * Detect and remove extra <result> wrapper around xml response.
+     * @param parser
+     * @param xpath
+     * @param 
+     * @return
+     */
+    public static  XmlNodeParser unwrapIfNeeded(
+            final XmlNodeParser parser,
+            final String xpath
+    ) {
+        return new NodeParser_unwrap(parser, xpath);
+    }
+
+    static class NodeParser_unwrap implements XmlNodeParser {
+        XmlNodeParser parser;
+        String           xpath;
+
+        public NodeParser_unwrap(final XmlNodeParser parser, final String xpath) {
+            this.parser = parser;
+            this.xpath = xpath;
+        }
+
+        @Override public T parseXmlNode(final Node node) {
+
+            Node sourceNode = unwrapResultElement(node, xpath);
+            return parser.parseXmlNode(sourceNode);
+        }
+    }
+
+    /**
+     * Test the node for matching the xpath string, if it doesnt match, attempt to prefix it with
+     * "result" and match that. If that matches, return the first child of the 'result' element.
+     *
+     * @param node
+     * @param xpath
+     *
+     * @return
+     */
+    public static Node unwrapResultElement(final Node node, final String xpath) {
+        Node sourceNode = node;
+        final Node tested = sourceNode.selectSingleNode(xpath);
+        if (null == tested && !xpath.startsWith("result")) {
+            //prepend /result
+            if (null != sourceNode.selectSingleNode("result" + xpath)) {
+                Node resultNode = sourceNode.selectSingleNode("result");
+                if (resultNode instanceof Element) {
+                    Element result = (Element) resultNode;
+                    if (result.elements().size() == 1) {
+
+                        Node node1 = (Node) result.elements().get(0);
+                        if (node1 instanceof Element) {
+                            sourceNode = node1;
+
+                            sourceNode.getParent().remove(sourceNode);
+                            DocumentHelper.createDocument((Element) sourceNode);
+                        }
+                    }
+                }
+            }
+        }
+        return sourceNode;
+    }
+}
diff --git a/src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java b/src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java
deleted file mode 100644
index 083b3d1..0000000
--- a/src/main/java/org/rundeck/api/parser/PagedResultParser_BugPatchV11.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package org.rundeck.api.parser;
-
-import org.dom4j.DocumentHelper;
-import org.dom4j.Element;
-import org.dom4j.Node;
-import org.rundeck.api.util.PagedResults;
-
-/**
- * WRaps a {@link PagedResultParser} to detect whether the result XML incorrectly is wrapped with a
- * <result> element.
- *
- * @author Greg Schueler 
- * @since 2014-11-07
- */
-public class PagedResultParser_BugPatchV11 implements XmlNodeParser> {
-    PagedResultParser parser;
-
-    public PagedResultParser_BugPatchV11(final PagedResultParser parser) {
-        this.parser = parser;
-    }
-
-    @Override public PagedResults parseXmlNode(final Node node) {
-        Node sourceNode = node;
-        final Node tested = sourceNode.selectSingleNode(parser.getXpath());
-        if (null == tested && !parser.getXpath().startsWith("result")) {
-            //prepend /result
-            if (null != sourceNode.selectSingleNode("result" + parser.getXpath())) {
-                sourceNode = sourceNode.selectSingleNode("result" + parser.getXpath());
-                sourceNode.getParent().remove(sourceNode);
-                DocumentHelper.createDocument((Element) sourceNode);
-            }
-        }
-        return parser.parseXmlNode(sourceNode);
-    }
-}

From 39fd86ae6db8c9dd8d1bd468f0c6312d9649cd52 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Mon, 10 Nov 2014 11:16:01 -0800
Subject: [PATCH 76/89] Trigger job handles buggy api v11 response

---
 src/main/java/org/rundeck/api/RundeckClient.java | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index a610219..7112a58 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1085,7 +1085,16 @@ public class RundeckClient implements Serializable {
         if(null!=jobRun.getAsUser()) {
             apiPath.param("asUser", jobRun.getAsUser());
         }
-        return new ApiCall(this).get(apiPath, new ExecutionParser(rootXpath()+"/executions/execution"));
+        return new ApiCall(this).get(apiPath,
+                                     APIV11Helper.unwrapIfNeeded(
+                                             new ExecutionParser(
+                                                     rootXpath() +
+                                                     "/executions/execution"
+                                             ), rootXpath() +
+                                                "/executions/execution"
+                                     )
+
+        );
     }
 
 

From 1f2ffeebacc0db1a2d58f3b38beea7787fb9e5f5 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Mon, 10 Nov 2014 12:08:39 -0800
Subject: [PATCH 77/89] getExecution handles buggy xml wrapper

---
 src/main/java/org/rundeck/api/RundeckClient.java | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java
index 7112a58..fb2ef09 100644
--- a/src/main/java/org/rundeck/api/RundeckClient.java
+++ b/src/main/java/org/rundeck/api/RundeckClient.java
@@ -1622,8 +1622,15 @@ public class RundeckClient implements Serializable {
     public RundeckExecution getExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException,
             RundeckApiTokenException, IllegalArgumentException {
         AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !");
-        return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString()),
-                                     new ExecutionParser(rootXpath()+"/executions/execution"));
+        return new ApiCall(this).get(
+                new ApiPathBuilder("/execution/", executionId.toString()),
+                APIV11Helper.unwrapIfNeeded(
+                        new ExecutionParser(
+                                rootXpath() + "/executions/execution"
+                        ),
+                        rootXpath() + "/executions/execution"
+                )
+        );
     }
 
     /**

From e98a3a0ad948e3256e7898a7288a8ea68d9989c0 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Mon, 10 Nov 2014 12:09:06 -0800
Subject: [PATCH 78/89] Add tests for api v11 responses

---
 .../org/rundeck/api/RundeckClientTest.java    | 130 ++++++++++++++++++
 .../betamax/tapes/get_execution.yaml          |  25 ++++
 .../betamax/tapes/get_execution_v11.yaml      |  24 ++++
 .../tapes/get_execution_v11_buggy.yaml        |  24 ++++
 .../tapes/trigger_adhoc_command_v11.yaml      |  43 ++++++
 .../trigger_adhoc_command_v11_buggy.yaml      |  42 ++++++
 .../tapes/trigger_adhoc_script_v11.yaml       |  45 ++++++
 .../tapes/trigger_adhoc_script_v11_buggy.yaml |  44 ++++++
 8 files changed, 377 insertions(+)
 create mode 100644 src/test/resources/betamax/tapes/get_execution.yaml
 create mode 100644 src/test/resources/betamax/tapes/get_execution_v11.yaml
 create mode 100644 src/test/resources/betamax/tapes/get_execution_v11_buggy.yaml
 create mode 100644 src/test/resources/betamax/tapes/trigger_adhoc_command_v11.yaml
 create mode 100644 src/test/resources/betamax/tapes/trigger_adhoc_command_v11_buggy.yaml
 create mode 100644 src/test/resources/betamax/tapes/trigger_adhoc_script_v11.yaml
 create mode 100644 src/test/resources/betamax/tapes/trigger_adhoc_script_v11_buggy.yaml

diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index 97b743d..04211f6 100644
--- a/src/test/java/org/rundeck/api/RundeckClientTest.java
+++ b/src/test/java/org/rundeck/api/RundeckClientTest.java
@@ -275,7 +275,47 @@ public class RundeckClientTest {
         }
         Assert.assertEquals(Arrays.asList("bob"), names);
     }
+    @Test
+    @Betamax(tape="get_execution")
+    public void getExecution() throws  Exception{
+        RundeckClient client = createClient(TEST_TOKEN_7, 5);
+        RundeckExecution execution = client.getExecution(945L);
+        Assert.assertEquals("echo test trigger_adhoc_command", execution.getDescription());
+        Assert.assertEquals("2 seconds", execution.getDuration());
+        Assert.assertEquals("test", execution.getProject());
+        Assert.assertEquals("admin", execution.getStartedBy());
+        Assert.assertEquals(null, execution.getJob());
+        Assert.assertEquals(null, execution.getAbortedBy());
+    }
+    @Test
+    @Betamax(tape="get_execution_v11")
+    public void getExecution_v11() throws  Exception{
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        RundeckExecution execution = client.getExecution(945L);
+        Assert.assertEquals("echo test trigger_adhoc_command", execution.getDescription());
+        Assert.assertEquals("2 seconds", execution.getDuration());
+        Assert.assertEquals("test", execution.getProject());
+        Assert.assertEquals("admin", execution.getStartedBy());
+        Assert.assertEquals(null, execution.getJob());
+        Assert.assertEquals(null, execution.getAbortedBy());
+    }
 
+    /**
+     * Test incorrect <result> wrapper is handled correctly
+     * @throws Exception
+     */
+    @Test
+    @Betamax(tape="get_execution_v11_buggy")
+    public void getExecution_v11_buggy() throws  Exception{
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        RundeckExecution execution = client.getExecution(945L);
+        Assert.assertEquals("echo test trigger_adhoc_command", execution.getDescription());
+        Assert.assertEquals("2 seconds", execution.getDuration());
+        Assert.assertEquals("test", execution.getProject());
+        Assert.assertEquals("admin", execution.getStartedBy());
+        Assert.assertEquals(null, execution.getJob());
+        Assert.assertEquals(null, execution.getAbortedBy());
+    }
     @Test
     @Betamax(tape = "get_executions",
              mode = TapeMode.READ_ONLY,
@@ -634,6 +674,11 @@ public class RundeckClientTest {
         Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
 
     }
+
+    /**
+     * API v11 request to trigger job, with expected xml response without <result> wrapper
+     * @throws Exception
+     */
     @Test
     @Betamax(tape = "trigger_job_basic_v11")
     public void triggerJobBasic_v11() throws Exception {
@@ -730,6 +775,43 @@ public class RundeckClientTest {
         Assert.assertEquals(RundeckExecution.ExecutionStatus.SUCCEEDED, test.getStatus());
     }
 
+    @Test
+    @Betamax(tape = "trigger_adhoc_command_v11_buggy")
+    public void triggerAdhocCommand_v11_buggy() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+        final RundeckExecution test
+                = client.triggerAdhocCommand(RunAdhocCommandBuilder.builder()
+                .setProject("test")
+                .setCommand("echo test trigger_adhoc_command")
+                .build());
+
+        Assert.assertEquals((Long) 945L, test.getId());
+        Assert.assertEquals(null, test.getArgstring());
+        Assert.assertEquals(null, test.getAbortedBy());
+        Assert.assertEquals("echo test trigger_adhoc_command", test.getDescription());
+        Assert.assertEquals("admin", test.getStartedBy());
+        Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
+    }
+    @Test
+    @Betamax(tape = "trigger_adhoc_command_v11")
+    public void triggerAdhocCommand_v11() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+
+        final RundeckExecution test
+                = client.triggerAdhocCommand(RunAdhocCommandBuilder.builder()
+                .setProject("test")
+                .setCommand("echo test trigger_adhoc_command")
+                .build());
+
+        Assert.assertEquals((Long) 946L, test.getId());
+        Assert.assertEquals(null, test.getArgstring());
+        Assert.assertEquals(null, test.getAbortedBy());
+        Assert.assertEquals("echo test trigger_adhoc_command", test.getDescription());
+        Assert.assertEquals("admin", test.getStartedBy());
+        Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
+    }
+
 
     @Test
     @Betamax(tape = "trigger_adhoc_command_as_user")
@@ -794,6 +876,54 @@ public class RundeckClientTest {
         Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
     }
 
+
+    /**
+     * Handle incorrect <result> wrapper for v11 response
+     * @throws Exception
+     */
+    @Test
+    @Betamax(tape = "trigger_adhoc_script_v11_buggy")
+    public void triggerAdhocScript_v11_buggy() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        String script = "#!/bin/bash\n" +
+                "echo test trigger_adhoc_script\n";
+        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(script.getBytes());
+
+        final RundeckExecution test
+                = client.triggerAdhocScript(RunAdhocScriptBuilder.builder().setProject("test").setScript
+                (byteArrayInputStream).build());
+
+        Assert.assertEquals((Long) 947L, test.getId());
+        Assert.assertEquals(null, test.getArgstring());
+        Assert.assertEquals(null, test.getAbortedBy());
+        Assert.assertEquals("#!/bin/bash\necho test trigger_adhoc_script", test.getDescription());
+        Assert.assertEquals("admin", test.getStartedBy());
+        Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
+    }
+    /**
+     * Handle v11 response without <result> wrapper
+     * @throws Exception
+     */
+    @Test
+    @Betamax(tape = "trigger_adhoc_script_v11")
+    public void triggerAdhocScript_v11() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        String script = "#!/bin/bash\n" +
+                "echo test trigger_adhoc_script\n";
+        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(script.getBytes());
+
+        final RundeckExecution test
+                = client.triggerAdhocScript(RunAdhocScriptBuilder.builder().setProject("test").setScript
+                (byteArrayInputStream).build());
+
+        Assert.assertEquals((Long) 948L, test.getId());
+        Assert.assertEquals(null, test.getArgstring());
+        Assert.assertEquals(null, test.getAbortedBy());
+        Assert.assertEquals("#!/bin/bash\necho test trigger_adhoc_script", test.getDescription());
+        Assert.assertEquals("admin", test.getStartedBy());
+        Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
+    }
+
     @Test
     @Betamax(tape = "trigger_adhoc_script_as_user")
     public void triggerAdhocScriptAsUser() throws Exception {
diff --git a/src/test/resources/betamax/tapes/get_execution.yaml b/src/test/resources/betamax/tapes/get_execution.yaml
new file mode 100644
index 0000000..d460f9d
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_execution.yaml
@@ -0,0 +1,25 @@
+!tape
+name: get_execution
+interactions:
+- recorded: 2014-11-10T19:43:41.415Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/5/execution/945
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 5
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1x4pkxxmmforonss2ga8ylvdc;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'true'
+    body: "\n  \n    \n      admin\n      2014-11-10T19:18:36Z\n\
+      \      2014-11-10T19:18:38Z\n      echo test trigger_adhoc_command\n      \n      \n        \n      \n  \
+      \  \n  \n"
diff --git a/src/test/resources/betamax/tapes/get_execution_v11.yaml b/src/test/resources/betamax/tapes/get_execution_v11.yaml
new file mode 100644
index 0000000..450b2d6
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_execution_v11.yaml
@@ -0,0 +1,24 @@
+!tape
+name: get_execution_v11
+interactions:
+- recorded: 2014-11-10T19:44:57.642Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/execution/945
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=yzyycrxo7utb154qrsuggbtp6;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbnMgY291bnQ9JzEnPgogIDxleGVjdXRpb24gaWQ9Jzk0NScgaHJlZj0naHR0cDovL2RpZ25hbjo0NDQwL2V4ZWN1dGlvbi9mb2xsb3cvOTQ1JyBzdGF0dXM9J3N1Y2NlZWRlZCcgcHJvamVjdD0ndGVzdCc+CiAgICA8dXNlcj5hZG1pbjwvdXNlcj4KICAgIDxkYXRlLXN0YXJ0ZWQgdW5peHRpbWU9JzE0MTU2NDcxMTY3MzknPjIwMTQtMTEtMTBUMTk6MTg6MzZaPC9kYXRlLXN0YXJ0ZWQ+CiAgICA8ZGF0ZS1lbmRlZCB1bml4dGltZT0nMTQxNTY0NzExODkzMSc+MjAxNC0xMS0xMFQxOToxODozOFo8L2RhdGUtZW5kZWQ+CiAgICA8ZGVzY3JpcHRpb24+ZWNobyB0ZXN0IHRyaWdnZXJfYWRob2NfY29tbWFuZDwvZGVzY3JpcHRpb24+CiAgICA8YXJnc3RyaW5nIC8+CiAgICA8c3VjY2Vzc2Z1bE5vZGVzPgogICAgICA8bm9kZSBuYW1lPSdkaWduYW4nIC8+CiAgICA8L3N1Y2Nlc3NmdWxOb2Rlcz4KICA8L2V4ZWN1dGlvbj4KPC9leGVjdXRpb25zPg==
diff --git a/src/test/resources/betamax/tapes/get_execution_v11_buggy.yaml b/src/test/resources/betamax/tapes/get_execution_v11_buggy.yaml
new file mode 100644
index 0000000..12eed0e
--- /dev/null
+++ b/src/test/resources/betamax/tapes/get_execution_v11_buggy.yaml
@@ -0,0 +1,24 @@
+!tape
+name: get_execution_v11_buggy
+interactions:
+- recorded: 2014-11-10T19:47:33.973Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/execution/945
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=prilmlypvi8r14cvs72xu9865;Path=/
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-10T19:18:36Z\n\
+      \      2014-11-10T19:18:38Z\n      echo test trigger_adhoc_command\n      \n      \n        \n      \n  \
+      \  \n  \n"
diff --git a/src/test/resources/betamax/tapes/trigger_adhoc_command_v11.yaml b/src/test/resources/betamax/tapes/trigger_adhoc_command_v11.yaml
new file mode 100644
index 0000000..c7dc62a
--- /dev/null
+++ b/src/test/resources/betamax/tapes/trigger_adhoc_command_v11.yaml
@@ -0,0 +1,43 @@
+!tape
+name: trigger_adhoc_command_v11
+interactions:
+- recorded: 2014-11-10T19:37:31.471Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/run/command?project=test&exec=echo+test+trigger_adhoc_command
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=1lzqzroq2oz9d1oiwcyb9qjsk8;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbiBpZD0nOTQ2JyAvPg==
+- recorded: 2014-11-10T19:37:31.807Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/execution/946
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbnMgY291bnQ9JzEnPgogIDxleGVjdXRpb24gaWQ9Jzk0NicgaHJlZj0naHR0cDovL2RpZ25hbjo0NDQwL2V4ZWN1dGlvbi9mb2xsb3cvOTQ2JyBzdGF0dXM9J3J1bm5pbmcnIHByb2plY3Q9J3Rlc3QnPgogICAgPHVzZXI+YWRtaW48L3VzZXI+CiAgICA8ZGF0ZS1zdGFydGVkIHVuaXh0aW1lPScxNDE1NjQ4MjUxMTg0Jz4yMDE0LTExLTEwVDE5OjM3OjMxWjwvZGF0ZS1zdGFydGVkPgogICAgPGRlc2NyaXB0aW9uPmVjaG8gdGVzdCB0cmlnZ2VyX2FkaG9jX2NvbW1hbmQ8L2Rlc2NyaXB0aW9uPgogICAgPGFyZ3N0cmluZyAvPgogIDwvZXhlY3V0aW9uPgo8L2V4ZWN1dGlvbnM+
diff --git a/src/test/resources/betamax/tapes/trigger_adhoc_command_v11_buggy.yaml b/src/test/resources/betamax/tapes/trigger_adhoc_command_v11_buggy.yaml
new file mode 100644
index 0000000..5f29a07
--- /dev/null
+++ b/src/test/resources/betamax/tapes/trigger_adhoc_command_v11_buggy.yaml
@@ -0,0 +1,42 @@
+!tape
+name: trigger_adhoc_command_v11_buggy
+interactions:
+- recorded: 2014-11-10T19:18:37.071Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/run/command?project=test&exec=echo+test+trigger_adhoc_command
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=jjryjdfuxtdw1808gclmb7uuj;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbiBpZD0nOTQ1JyAvPg==
+- recorded: 2014-11-10T19:18:37.444Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/execution/945
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-10T19:18:36Z\n\
+      \      echo test trigger_adhoc_command\n      \n    \n  \n"
diff --git a/src/test/resources/betamax/tapes/trigger_adhoc_script_v11.yaml b/src/test/resources/betamax/tapes/trigger_adhoc_script_v11.yaml
new file mode 100644
index 0000000..8b133c0
--- /dev/null
+++ b/src/test/resources/betamax/tapes/trigger_adhoc_script_v11.yaml
@@ -0,0 +1,45 @@
+!tape
+name: trigger_adhoc_script_v11
+interactions:
+- recorded: 2014-11-10T20:04:36.573Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/11/run/script?project=test
+    headers:
+      Accept: text/xml
+      Content-Type: multipart/form-data; boundary=W2S1I8XOmvOWsdnLEHprcw3N6cQveJEm_aV17Oz
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=18uoooauscne2eqtuq466djm;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbiBpZD0nOTQ4JyAvPg==
+- recorded: 2014-11-10T20:04:36.876Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/execution/948
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbnMgY291bnQ9JzEnPgogIDxleGVjdXRpb24gaWQ9Jzk0OCcgaHJlZj0naHR0cDovL2RpZ25hbjo0NDQwL2V4ZWN1dGlvbi9mb2xsb3cvOTQ4JyBzdGF0dXM9J3J1bm5pbmcnIHByb2plY3Q9J3Rlc3QnPgogICAgPHVzZXI+YWRtaW48L3VzZXI+CiAgICA8ZGF0ZS1zdGFydGVkIHVuaXh0aW1lPScxNDE1NjQ5ODc2MzA1Jz4yMDE0LTExLTEwVDIwOjA0OjM2WjwvZGF0ZS1zdGFydGVkPgogICAgPGRlc2NyaXB0aW9uPiMhL2Jpbi9iYXNoCmVjaG8gdGVzdCB0cmlnZ2VyX2FkaG9jX3NjcmlwdAo8L2Rlc2NyaXB0aW9uPgogICAgPGFyZ3N0cmluZyAvPgogIDwvZXhlY3V0aW9uPgo8L2V4ZWN1dGlvbnM+
diff --git a/src/test/resources/betamax/tapes/trigger_adhoc_script_v11_buggy.yaml b/src/test/resources/betamax/tapes/trigger_adhoc_script_v11_buggy.yaml
new file mode 100644
index 0000000..970ba9a
--- /dev/null
+++ b/src/test/resources/betamax/tapes/trigger_adhoc_script_v11_buggy.yaml
@@ -0,0 +1,44 @@
+!tape
+name: trigger_adhoc_script_v11_buggy
+interactions:
+- recorded: 2014-11-10T19:54:03.631Z
+  request:
+    method: POST
+    uri: http://rundeck.local:4440/api/11/run/script?project=test
+    headers:
+      Accept: text/xml
+      Content-Type: multipart/form-data; boundary=7ijcD_EYqXFXKLau3Bg9Oy0PIqf37Vn
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      Transfer-Encoding: chunked
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: application/xml;charset=UTF-8
+      Expires: Thu, 01 Jan 1970 00:00:00 GMT
+      Server: Jetty(7.6.0.v20120127)
+      Set-Cookie: JSESSIONID=z5e76c45acya1h081pryh4avz;Path=/
+      X-Rundeck-API-Version: '12'
+      X-Rundeck-API-XML-Response-Wrapper: 'false'
+    body: !!binary |-
+      PGV4ZWN1dGlvbiBpZD0nOTQ3JyAvPg==
+- recorded: 2014-11-10T19:54:03.770Z
+  request:
+    method: GET
+    uri: http://rundeck.local:4440/api/11/execution/947
+    headers:
+      Accept: text/xml
+      Host: rundeck.local:4440
+      Proxy-Connection: Keep-Alive
+      User-Agent: RunDeck API Java Client 11
+      X-RunDeck-Auth-Token: 8Dp9op111ER6opsDRkddvE86K9sE499s
+  response:
+    status: 200
+    headers:
+      Content-Type: text/xml;charset=UTF-8
+      Server: Jetty(7.6.0.v20120127)
+      X-Rundeck-API-Version: '12'
+    body: "\n  \n    \n      admin\n      2014-11-10T19:54:03Z\n\
+      \      #!/bin/bash\necho test trigger_adhoc_script\n\n      \n    \n  \n"

From ea5f82714d9e090f893da6fb0a0d486f5fac2663 Mon Sep 17 00:00:00 2001
From: Sylvain Bugat 
Date: Sun, 23 Nov 2014 15:42:14 +0100
Subject: [PATCH 79/89] Add maven prerequisites for dependencies check

Maven plugin version upgrade 1.2 -> 2.1
Add dependencies-check-rules.xml to ignore unreleased versions
---
 dependencies-check-rules.xml | 13 +++++++++++++
 pom.xml                      | 36 +++++++++++++++++++++++++-----------
 2 files changed, 38 insertions(+), 11 deletions(-)
 create mode 100644 dependencies-check-rules.xml

diff --git a/dependencies-check-rules.xml b/dependencies-check-rules.xml
new file mode 100644
index 0000000..9721dde
--- /dev/null
+++ b/dependencies-check-rules.xml
@@ -0,0 +1,13 @@
+
+
+	
+	
+		.*[\.-](?i)alpha[0-9]*$
+		.*[\.-](?i)b(eta)?-?[0-9]*$
+		.*[\.-](?i)rc?[0-9]*$
+		.*[\.-](?i)draft.*$
+	
+
diff --git a/pom.xml b/pom.xml
index 7d506cd..b125fb5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,6 +17,10 @@
 
   4.0.0
 
+  
+     2.2.1
+  
+
   
   
     org.sonatype.oss
@@ -88,6 +92,16 @@
     1.6
     UTF-8
 
+    
+    4.1.2
+    2.6
+    2.1
+    1.6.1
+    1.1.1
+    4.10
+    1.0
+    1.8.4
+
     
     1.7
     2.2.2
@@ -114,7 +128,7 @@
     2.1.2
     2.10
     2.4
-    1.2
+    2.1
   
 
   
@@ -392,7 +406,7 @@
           
         
         
-          mercury
+          file:./dependencies-check-rules.xml
         
       
       
@@ -423,52 +437,52 @@
     
       org.apache.httpcomponents
       httpclient
-      4.1.2
+      ${apache.httpcomponents.version}
     
     
       org.apache.httpcomponents
       httpmime
-      4.1.2
+      ${apache.httpcomponents.version}
     
     
     
       commons-lang
       commons-lang
-      2.6
+      ${commons-lang.version}
     
     
       commons-io
       commons-io
-      2.1
+      ${commons-io.version}
     
     
     
       dom4j
       dom4j
-      1.6.1
+      ${dom4j.version}
     
     
       jaxen
       jaxen
-      1.1.1
+      ${jaxen.version}
     
     
     
       junit
       junit
-      4.10
+      ${junit.version}
       test
     
     
       com.github.robfletcher
       betamax
-      1.0
+      ${betamax.version}
       test
     
     
       org.codehaus.groovy
       groovy-all
-      1.8.4
+      ${groovy.version}
       test
     
   

From b3839f089b9b0db866bab01606781a0107d850b1 Mon Sep 17 00:00:00 2001
From: Sylvain Bugat 
Date: Mon, 24 Nov 2014 21:59:30 +0100
Subject: [PATCH 80/89] Httpcomponents upgrade
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

4.1.2 -> 4.3.6: tested OK on unit tests and on RundeckMonitor
OWASP A9 fix know vulnérabilies:
CVE-2014-3577: org.apache.http.conn.ssl.AbstractVerifier in Apache
HttpComponents HttpClient before 4.3.5
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index b125fb5..9977654 100644
--- a/pom.xml
+++ b/pom.xml
@@ -93,7 +93,7 @@
     UTF-8
 
     
-    4.1.2
+    4.3.6
     2.6
     2.1
     1.6.1

From c1a1f3c365e1dc9a3b8314c6e3030229090dbe29 Mon Sep 17 00:00:00 2001
From: Sylvain Bugat 
Date: Mon, 24 Nov 2014 22:02:56 +0100
Subject: [PATCH 81/89] Deprecation fix apache.httpcomponents

fix deprecation due to apache.httpcomponents upgrade and fixing some
code warnings
---
 src/main/java/org/rundeck/api/ApiCall.java | 39 +++++++++++-----------
 1 file changed, 19 insertions(+), 20 deletions(-)

diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java
index 71a9808..da5c5a6 100644
--- a/src/main/java/org/rundeck/api/ApiCall.java
+++ b/src/main/java/org/rundeck/api/ApiCall.java
@@ -37,7 +37,6 @@ import org.apache.http.params.HttpProtocolParams;
 import org.apache.http.protocol.HTTP;
 import org.apache.http.protocol.HttpContext;
 import org.apache.http.util.EntityUtils;
-import org.dom4j.Document;
 import org.rundeck.api.RundeckApiException.RundeckApiLoginException;
 import org.rundeck.api.RundeckApiException.RundeckApiTokenException;
 import org.rundeck.api.parser.ParserHelper;
@@ -59,7 +58,7 @@ import java.util.Map.Entry;
 
 /**
  * Class responsible for making the HTTP API calls
- * 
+ *
  * @author Vincent Behar
  */
 class ApiCall {
@@ -72,10 +71,10 @@ class ApiCall {
 
     /** {@link RundeckClient} instance holding the RunDeck url and the credentials */
     private final RundeckClient client;
-    
+
     /**
      * Build a new instance, linked to the given RunDeck client
-     * 
+     *
      * @param client holding the RunDeck url and the credentials
      * @throws IllegalArgumentException if client is null
      */
@@ -87,7 +86,7 @@ class ApiCall {
 
     /**
      * Try to "ping" the RunDeck instance to see if it is alive
-     * 
+     *
      * @throws RundeckApiException if the ping fails
      */
     public void ping() throws RundeckApiException {
@@ -127,7 +126,7 @@ class ApiCall {
 
     /**
      * Test the login-based authentication on the RunDeck instance
-     * 
+     *
      * @throws RundeckApiLoginException if the login fails
      * @see #testAuth()
      */
@@ -144,7 +143,7 @@ class ApiCall {
 
     /**
      * Test the token-based authentication on the RunDeck instance
-     * 
+     *
      * @throws RundeckApiTokenException if the token is invalid
      * @see #testAuth()
      */
@@ -161,7 +160,7 @@ class ApiCall {
     /**
      * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
      * API call. At the end, the given parser will be used to convert the response to a more useful result object.
-     * 
+     *
      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
      * @param parser used to parse the response
      * @return the result of the call, as formatted by the parser
@@ -181,7 +180,7 @@ class ApiCall {
     /**
      * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
      * API call.
-     * 
+     *
      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
      * @return a new {@link InputStream} instance, not linked with network resources
      * @throws RundeckApiException in case of error when calling the API
@@ -206,7 +205,7 @@ class ApiCall {
     /**
      * Execute an HTTP GET request to the RunDeck instance, on the given path. We will login first, and then execute the
      * API call without appending the API_ENDPOINT to the URL.
-     * 
+     *
      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
      * @return a new {@link InputStream} instance, not linked with network resources
      * @throws RundeckApiException in case of error when calling the API
@@ -248,11 +247,11 @@ class ApiCall {
             return get(apiPath, parser);
         }
     }
-    
+
     /**
      * Execute an HTTP POST request to the RunDeck instance, on the given path. We will login first, and then execute
      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
-     * 
+     *
      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
      * @param parser used to parse the response
      * @return the result of the call, as formatted by the parser
@@ -322,7 +321,7 @@ class ApiCall {
     /**
      * Execute an HTTP DELETE request to the RunDeck instance, on the given path. We will login first, and then execute
      * the API call. At the end, the given parser will be used to convert the response to a more useful result object.
-     * 
+     *
      * @param apiPath on which we will make the HTTP request - see {@link ApiPathBuilder}
      * @param parser used to parse the response
      * @return the result of the call, as formatted by the parser
@@ -354,7 +353,7 @@ class ApiCall {
     /**
      * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call. At the end,
      * the given parser will be used to convert the response to a more useful result object.
-     * 
+     *
      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
      * @param parser used to parse the response
      * @return the result of the call, as formatted by the parser
@@ -400,7 +399,7 @@ class ApiCall {
     }
     /**
      * Execute an HTTP request to the RunDeck instance. We will login first, and then execute the API call.
-     * 
+     *
      * @param request to execute. see {@link HttpGet}, {@link HttpDelete}, and so on...
      * @return a new {@link InputStream} instance, not linked with network resources
      * @throws RundeckApiException in case of error when calling the API
@@ -594,7 +593,7 @@ class ApiCall {
     /**
      * Do the actual work of login, using the given {@link HttpClient} instance. You'll need to re-use this instance
      * when making API calls (such as running a job). Only use this in case of login-based authentication.
-     * 
+     *
      * @param httpClient pre-instantiated
      * @throws RundeckApiLoginException if the login failed
      */
@@ -639,11 +638,11 @@ class ApiCall {
         while (true) {
             try {
                 HttpPost postLogin = new HttpPost(location);
-                List params = new ArrayList();
+                List params = new ArrayList();
                 params.add(new BasicNameValuePair("j_username", client.getLogin()));
                 params.add(new BasicNameValuePair("j_password", client.getPassword()));
                 params.add(new BasicNameValuePair("action", "login"));
-                postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8));
+                postLogin.setEntity(new UrlEncodedFormEntity(params, Consts.UTF_8));
                 HttpResponse response = httpClient.execute(postLogin);
 
                 if (response.getStatusLine().getStatusCode() / 100 == 3) {
@@ -663,7 +662,7 @@ class ApiCall {
                 }
 
                 try {
-                    String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8);
+                    String content = EntityUtils.toString(response.getEntity(), Consts.UTF_8);
                     if (StringUtils.contains(content, "j_security_check")) {
                         throw new RundeckApiLoginException("Login failed for user " + client.getLogin());
                     }
@@ -689,7 +688,7 @@ class ApiCall {
 
     /**
      * Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates
-     * 
+     *
      * @return an {@link HttpClient} instance - won't be null
      */
     private HttpClient instantiateHttpClient() {

From 008a8c3ce42129a32a8ec2e50fa655f7a1a1639d Mon Sep 17 00:00:00 2001
From: Sylvain Bugat 
Date: Mon, 24 Nov 2014 22:28:06 +0100
Subject: [PATCH 82/89] Spell correction

---
 dependencies-check-rules.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/dependencies-check-rules.xml b/dependencies-check-rules.xml
index 9721dde..ead5a09 100644
--- a/dependencies-check-rules.xml
+++ b/dependencies-check-rules.xml
@@ -3,7 +3,7 @@
 	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 	xsi:schemaLocation="http://mojo.codehaus.org/versions-maven-plugin/rule/2.0.0 http://mojo.codehaus.org/versions-maven-plugin/xsd/rule-2.0.0.xsd">
 
-	
+	
 	
 		.*[\.-](?i)alpha[0-9]*$
 		.*[\.-](?i)b(eta)?-?[0-9]*$

From 92d15c0748a0d61e99524ea183e33ccf73283413 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Mon, 24 Nov 2014 14:05:00 -0800
Subject: [PATCH 83/89] Update to version 12.0-SNAPSHOT

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 9977654..15770f2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  11.2-SNAPSHOT
+  12.0-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From 5bf4be0bc82724016ce417da0fbc4eaeea87a1f2 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Tue, 25 Nov 2014 10:38:49 -0800
Subject: [PATCH 84/89] Update changelog for v 12.0

---
 src/changes/changes.xml | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 6f25616..98deb4a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -19,9 +19,21 @@
           xsi:schemaLocation="http://maven.apache.org/changes/1.0.0 http://maven.apache.org/xsd/changes-1.0.0.xsd">
   
     Changelog
-    Vincent Behar
+    Greg Schueler
   
   
+      
+          
+              Pull Request #18: OWASP A9 fix know vulnerabilities: CVE-2014-3577: in Apache
+              HttpComponents HttpClient before 4.3.5.
+          
+          
+              Add API v12 support
+          
+          
+              Issue #17: Handle incorrect API v11 responses
+          
+      
       
           
               Issue #14: deleteJob fails

From 68717ae8641311047a05befd251e0269880db99e Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Tue, 25 Nov 2014 10:56:45 -0800
Subject: [PATCH 85/89] Update download page

---
 src/site/confluence/download.confluence | 7 ++-----
 1 file changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/site/confluence/download.confluence b/src/site/confluence/download.confluence
index 5929431..b959285 100644
--- a/src/site/confluence/download.confluence
+++ b/src/site/confluence/download.confluence
@@ -7,13 +7,10 @@ The library is hosted on the [Maven Central Repository|http://search.maven.org/]
 
 You can see all versions and download the files with this query : [g:"org.rundeck" AND a:"rundeck-api-java-client"|http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.rundeck%22%20AND%20a%3A%22rundeck-api-java-client%22].
 
-Alternatively, you can download [releases|https://oss.sonatype.org/content/repositories/releases/org/rundeck/rundeck-api-java-client/] or [snapshots|https://oss.sonatype.org/content/repositories/snapshots/org/rundeck/rundeck-api-java-client/] from [Sonatype Nexus OSS instance|https://oss.sonatype.org/] (which is then synchronized to Maven Central).
+Alternatively, you can download [releases|https://oss.sonatype.org/content/repositories/releases/org/rundeck/rundeck-api-java-client/] from [Sonatype Nexus OSS instance|https://oss.sonatype.org/] (which is then synchronized to Maven Central).
 
 h2. Manual download
 
 If you want to use this library from a [scripting language|./scripting.html], it is often easier to download a single *jar* file with all dependencies included (named "*rundeck-api-java-client-VERSION-jar-with-dependencies.jar *")
 
-You can download such files on GitHub : [https://github.com/vbehar/rundeck-api-java-client/downloads] (for each release)
-
-You can also download the latest version (not yet released) on Jenkins : [https://rundeck-api-java-client.ci.cloudbees.com/job/master/lastSuccessfulBuild/artifact/target/]
-
+You can download such files on GitHub : [https://github.com/rundeck/rundeck-api-java-client/releases] (for each release)

From 19527ea32513ae0e4e39f4afa9bf09f8075b6dc9 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Tue, 25 Nov 2014 10:59:16 -0800
Subject: [PATCH 86/89] Fix creation of rundeck client in examples

---
 src/site/confluence/groovy.confluence | 22 +++++++++++-----------
 src/site/confluence/jruby.confluence  | 20 ++++++++++----------
 src/site/confluence/jython.confluence | 18 +++++++++---------
 3 files changed, 30 insertions(+), 30 deletions(-)

diff --git a/src/site/confluence/groovy.confluence b/src/site/confluence/groovy.confluence
index 6dc06e5..e5366a4 100644
--- a/src/site/confluence/groovy.confluence
+++ b/src/site/confluence/groovy.confluence
@@ -11,10 +11,10 @@ Save the following script in a file named "{{rundeck.groovy}}", and execute it w
 
 {code}
 // we use Grape (Ivy) to download the lib (and its dependencies) from Maven Central Repository
-@Grab(group='org.rundeck', module='rundeck-api-java-client', version='2.0')
+@Grab(group='org.rundeck', module='rundeck-api-java-client', version='12.0')
 import org.rundeck.api.RundeckClient
 
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 println "RunDeck uptime : ${rundeck.systemInfo.uptime}"
 println "All RunDeck projects : ${rundeck.projects}"
@@ -28,7 +28,7 @@ You can also [download|./download.html] the lib and all its dependencies in 1 bi
 {code}
 import org.rundeck.api.RundeckClient
 
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 println "RunDeck uptime : ${rundeck.systemInfo.uptime}"
 println "All RunDeck projects : ${rundeck.projects}"
@@ -44,18 +44,18 @@ Starting with RunDeck API 2, there are 2 ways to authenticate :
 * the *token-based authentication* : with a unique token that you can generate from the RunDeck webUI
 
 {code}
-// using login-based authentication (admin/admin is the default login/password for a new RunDeck instance) :
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin");
+// using login-based authentication (admin/admin is the default login/password for a new Rundeck instance) :
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // using token-based authentication :
-rundeck = new RundeckClient("http://localhost:4440", "PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD");
+rundeck = RundeckClient.builder().url("http://localhost:4440").token("PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD").build()
 {code}
 
 h2. Running a job
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // find a job from its name, group and project
 job = rundeck.findJob("my-project", "main-group/sub-group", "job-name")
@@ -80,7 +80,7 @@ h2. Running an ad-hoc command
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // trigger the execution of the "uptime" command on the RunDeck server
 execution = rundeck.triggerAdhocCommand("my-project", "uptime")
@@ -93,7 +93,7 @@ h2. Running an ad-hoc script
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // trigger the execution of a custom bash script on the RunDeck server
 execution = rundeck.triggerAdhocScript("my-project", "/tmp/my-script.sh")
@@ -106,7 +106,7 @@ h2. Exporting jobs
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 rundeck.exportJobsToFile("/tmp/jobs.xml", "xml", "my-project")
 rundeck.exportJobToFile("/tmp/job.yaml", "yaml", "job-id")
@@ -116,7 +116,7 @@ h2. Importing jobs
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = new RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 result = rundeck.importJobs("/tmp/jobs.xml", "xml")
 println "${result.succeededJobs.size} jobs successfully imported, ${result.skippedJobs.size} jobs skipped, and ${result.failedJobs.size} jobs failed"
diff --git a/src/site/confluence/jruby.confluence b/src/site/confluence/jruby.confluence
index cedb4ae..be1abf4 100644
--- a/src/site/confluence/jruby.confluence
+++ b/src/site/confluence/jruby.confluence
@@ -14,7 +14,7 @@ require 'java'
 require '/path/to/rundeck-api-java-client-VERSION-jar-with-dependencies.jar'
 import org.rundeck.api.RundeckClient
 
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 puts "RunDeck uptime : #{rundeck.systemInfo.uptime}"
 puts "All RunDeck projects : #{rundeck.projects}"
@@ -28,7 +28,7 @@ You can also add the library to the classpath : save the following script in a f
 {code}
 import org.rundeck.api.RundeckClient
 
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 puts "RunDeck uptime : #{rundeck.systemInfo.uptime}"
 puts "All RunDeck projects : #{rundeck.projects}"
@@ -44,11 +44,11 @@ Starting with RunDeck API 2, there are 2 ways to authenticate :
 * the *token-based authentication* : with a unique token that you can generate from the RunDeck webUI
 
 {code}
-// using login-based authentication (admin/admin is the default login/password for a new RunDeck instance) :
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin");
+// using login-based authentication (admin/admin is the default login/password for a new Rundeck instance) :
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // using token-based authentication :
-rundeck = RundeckClient.new("http://localhost:4440", "PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD");
+rundeck = RundeckClient.builder().url("http://localhost:4440").token("PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD").build()
 {code}
 
 h2. Running a job
@@ -58,7 +58,7 @@ import org.rundeck.api.RundeckClient
 import org.rundeck.api.OptionsBuilder
 import org.rundeck.api.NodeFiltersBuilder
 
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // find a job from its name, group and project
 job = rundeck.findJob("my-project", "main-group/sub-group", "job-name")
@@ -85,7 +85,7 @@ h2. Running an ad-hoc command
 import org.rundeck.api.RundeckClient
 import org.rundeck.api.NodeFiltersBuilder
 
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // trigger the execution of the "uptime" command on the RunDeck server
 execution = rundeck.triggerAdhocCommand("my-project", "uptime")
@@ -101,7 +101,7 @@ import org.rundeck.api.RundeckClient
 import org.rundeck.api.OptionsBuilder
 import org.rundeck.api.NodeFiltersBuilder
 
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // trigger the execution of a custom bash script on the RunDeck server
 execution = rundeck.triggerAdhocScript("my-project", "/tmp/my-script.sh")
@@ -114,7 +114,7 @@ h2. Exporting jobs
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 rundeck.exportJobsToFile("/tmp/jobs.xml", "xml", "my-project")
 rundeck.exportJobToFile("/tmp/job.yaml", "yaml", "job-id")
@@ -124,7 +124,7 @@ h2. Importing jobs
 
 {code}
 import org.rundeck.api.RundeckClient
-rundeck = RundeckClient.new("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 result = rundeck.importJobs("/tmp/jobs.xml", "xml")
 puts "#{result.succeededJobs.size} jobs successfully imported, #{result.skippedJobs.size} jobs skipped, and #{result.failedJobs.size} jobs failed"
diff --git a/src/site/confluence/jython.confluence b/src/site/confluence/jython.confluence
index b2dbc94..88019aa 100644
--- a/src/site/confluence/jython.confluence
+++ b/src/site/confluence/jython.confluence
@@ -12,7 +12,7 @@ Save the following script in a file named "{{rundeck.py}}", and execute it with
 {code}
 from org.rundeck.api import RundeckClient
 
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 print("RunDeck uptime : %s" % rundeck.systemInfo.uptime)
 print("All RunDeck projects : %s" % rundeck.projects)
@@ -28,11 +28,11 @@ Starting with RunDeck API 2, there are 2 ways to authenticate :
 * the *token-based authentication* : with a unique token that you can generate from the RunDeck webUI
 
 {code}
-// using login-based authentication (admin/admin is the default login/password for a new RunDeck instance) :
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin");
+// using login-based authentication (admin/admin is the default login/password for a new Rundeck instance) :
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // using token-based authentication :
-rundeck = RundeckClient("http://localhost:4440", "PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD");
+rundeck = RundeckClient.builder().url("http://localhost:4440").token("PDDNKo5VE29kpk4prOUDr2rsKdRkEvsD").build()
 {code}
 
 h2. Running a job
@@ -42,7 +42,7 @@ from org.rundeck.api import RundeckClient
 from org.rundeck.api import OptionsBuilder
 from org.rundeck.api import NodeFiltersBuilder
 
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // find a job from its name, group and project
 job = rundeck.findJob("my-project", "main-group/sub-group", "job-name")
@@ -69,7 +69,7 @@ h2. Running an ad-hoc command
 from org.rundeck.api import RundeckClient
 from org.rundeck.api import NodeFiltersBuilder
 
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // trigger the execution of the "uptime" command on the RunDeck server
 execution = rundeck.triggerAdhocCommand("my-project", "uptime")
@@ -85,7 +85,7 @@ from org.rundeck.api import RundeckClient
 from org.rundeck.api import OptionsBuilder
 from org.rundeck.api import NodeFiltersBuilder
 
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 // trigger the execution of a custom bash script on the RunDeck server
 execution = rundeck.triggerAdhocScript("my-project", "/tmp/my-script.sh")
@@ -98,7 +98,7 @@ h2. Exporting jobs
 
 {code}
 from org.rundeck.api import RundeckClient
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 rundeck.exportJobsToFile("/tmp/jobs.xml", "xml", "my-project")
 rundeck.exportJobToFile("/tmp/job.yaml", "yaml", "job-id")
@@ -108,7 +108,7 @@ h2. Importing jobs
 
 {code}
 from org.rundeck.api import RundeckClient
-rundeck = RundeckClient("http://localhost:4440", "admin", "admin")
+rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
 result = rundeck.importJobs("/tmp/jobs.xml", "xml")
 print("%s jobs successfully imported, %s jobs skipped, and %s jobs failed" % (result.succeededJobs.size, result.skippedJobs.size, result.failedJobs.size))

From d612f3c88bb554adc85499d24c1926a49a7c1134 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Tue, 25 Nov 2014 10:59:27 -0800
Subject: [PATCH 87/89] text fix naming RunDeck to Rundeck

---
 src/site/confluence/groovy.confluence | 32 +++++++++---------
 src/site/confluence/jruby.confluence  | 32 +++++++++---------
 src/site/confluence/jython.confluence | 22 ++++++------
 src/site/confluence/status.confluence | 48 +++++++++++++--------------
 src/site/site.xml                     |  6 ++--
 5 files changed, 70 insertions(+), 70 deletions(-)

diff --git a/src/site/confluence/groovy.confluence b/src/site/confluence/groovy.confluence
index e5366a4..20fd4d4 100644
--- a/src/site/confluence/groovy.confluence
+++ b/src/site/confluence/groovy.confluence
@@ -1,5 +1,5 @@
 
-h1. Using the RunDeck API from Groovy scripts
+h1. Using the Rundeck API from Groovy scripts
 
 Here are some examples of what you can do with this lib and a few lines of [Groovy|http://groovy.codehaus.org/].
 
@@ -16,11 +16,11 @@ import org.rundeck.api.RundeckClient
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-println "RunDeck uptime : ${rundeck.systemInfo.uptime}"
-println "All RunDeck projects : ${rundeck.projects}"
-println "All RunDeck nodes : ${rundeck.nodes}"
-println "All RunDeck jobs : ${rundeck.jobs}"
-println "All RunDeck running executions : ${rundeck.runningExecutions}"
+println "Rundeck uptime : ${rundeck.systemInfo.uptime}"
+println "All Rundeck projects : ${rundeck.projects}"
+println "All Rundeck nodes : ${rundeck.nodes}"
+println "All Rundeck jobs : ${rundeck.jobs}"
+println "All Rundeck running executions : ${rundeck.runningExecutions}"
 {code}
 
 You can also [download|./download.html] the lib and all its dependencies in 1 big jar file, and add it to your classpath before running your script : save the following script in a file named "{{rundeck.groovy}}", and execute it with "{{groovy -cp /path/to/rundeck-api-java-client-VERSION-jar-with-dependencies.jar rundeck.groovy}}".
@@ -30,18 +30,18 @@ import org.rundeck.api.RundeckClient
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-println "RunDeck uptime : ${rundeck.systemInfo.uptime}"
-println "All RunDeck projects : ${rundeck.projects}"
-println "All RunDeck nodes : ${rundeck.nodes}"
-println "All RunDeck jobs : ${rundeck.jobs}"
-println "All RunDeck running executions : ${rundeck.runningExecutions}"
+println "Rundeck uptime : ${rundeck.systemInfo.uptime}"
+println "All Rundeck projects : ${rundeck.projects}"
+println "All Rundeck nodes : ${rundeck.nodes}"
+println "All Rundeck jobs : ${rundeck.jobs}"
+println "All Rundeck running executions : ${rundeck.runningExecutions}"
 {code}
 
 h2. Authentication
 
-Starting with RunDeck API 2, there are 2 ways to authenticate :
+Starting with Rundeck API 2, there are 2 ways to authenticate :
 * the *login-based authentication* : with your login and password
-* the *token-based authentication* : with a unique token that you can generate from the RunDeck webUI
+* the *token-based authentication* : with a unique token that you can generate from the Rundeck webUI
 
 {code}
 // using login-based authentication (admin/admin is the default login/password for a new Rundeck instance) :
@@ -82,7 +82,7 @@ h2. Running an ad-hoc command
 import org.rundeck.api.RundeckClient
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-// trigger the execution of the "uptime" command on the RunDeck server
+// trigger the execution of the "uptime" command on the Rundeck server
 execution = rundeck.triggerAdhocCommand("my-project", "uptime")
 
 // run the "uptime" command on all unix nodes
@@ -95,7 +95,7 @@ h2. Running an ad-hoc script
 import org.rundeck.api.RundeckClient
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-// trigger the execution of a custom bash script on the RunDeck server
+// trigger the execution of a custom bash script on the Rundeck server
 execution = rundeck.triggerAdhocScript("my-project", "/tmp/my-script.sh")
 
 // run a bash script (with options) on all unix nodes
@@ -124,5 +124,5 @@ println "${result.succeededJobs.size} jobs successfully imported, ${result.skipp
 
 h2. And more...
 
-See the API documentation of the [RundeckClient|./apidocs/reference/org/rundeck/api/RundeckClient.html] class for more interactions with your RunDeck instance...
+See the API documentation of the [RundeckClient|./apidocs/reference/org/rundeck/api/RundeckClient.html] class for more interactions with your Rundeck instance...
 
diff --git a/src/site/confluence/jruby.confluence b/src/site/confluence/jruby.confluence
index be1abf4..d761a36 100644
--- a/src/site/confluence/jruby.confluence
+++ b/src/site/confluence/jruby.confluence
@@ -1,5 +1,5 @@
 
-h1. Using the RunDeck API from JRuby scripts
+h1. Using the Rundeck API from JRuby scripts
 
 Here are some examples of what you can do with this lib and a few lines of [Ruby|http://www.jruby.org/] (for the rubyist that don't fear running on the JVM !)
 
@@ -16,11 +16,11 @@ import org.rundeck.api.RundeckClient
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-puts "RunDeck uptime : #{rundeck.systemInfo.uptime}"
-puts "All RunDeck projects : #{rundeck.projects}"
-puts "All RunDeck nodes : #{rundeck.nodes}"
-puts "All RunDeck jobs : #{rundeck.jobs}"
-puts "All RunDeck running executions : #{rundeck.runningExecutions}"
+puts "Rundeck uptime : #{rundeck.systemInfo.uptime}"
+puts "All Rundeck projects : #{rundeck.projects}"
+puts "All Rundeck nodes : #{rundeck.nodes}"
+puts "All Rundeck jobs : #{rundeck.jobs}"
+puts "All Rundeck running executions : #{rundeck.runningExecutions}"
 {code}
 
 You can also add the library to the classpath : save the following script in a file named "{{rundeck.rb}}", and execute it with "{{jruby -rjava -J-cp /path/to/rundeck-api-java-client-VERSION-jar-with-dependencies.jar rundeck.rb}}".
@@ -30,18 +30,18 @@ import org.rundeck.api.RundeckClient
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-puts "RunDeck uptime : #{rundeck.systemInfo.uptime}"
-puts "All RunDeck projects : #{rundeck.projects}"
-puts "All RunDeck nodes : #{rundeck.nodes}"
-puts "All RunDeck jobs : #{rundeck.jobs}"
-puts "All RunDeck running executions : #{rundeck.runningExecutions}"
+puts "Rundeck uptime : #{rundeck.systemInfo.uptime}"
+puts "All Rundeck projects : #{rundeck.projects}"
+puts "All Rundeck nodes : #{rundeck.nodes}"
+puts "All Rundeck jobs : #{rundeck.jobs}"
+puts "All Rundeck running executions : #{rundeck.runningExecutions}"
 {code}
 
 h2. Authentication
 
-Starting with RunDeck API 2, there are 2 ways to authenticate :
+Starting with Rundeck API 2, there are 2 ways to authenticate :
 * the *login-based authentication* : with your login and password
-* the *token-based authentication* : with a unique token that you can generate from the RunDeck webUI
+* the *token-based authentication* : with a unique token that you can generate from the Rundeck webUI
 
 {code}
 // using login-based authentication (admin/admin is the default login/password for a new Rundeck instance) :
@@ -87,7 +87,7 @@ import org.rundeck.api.NodeFiltersBuilder
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-// trigger the execution of the "uptime" command on the RunDeck server
+// trigger the execution of the "uptime" command on the Rundeck server
 execution = rundeck.triggerAdhocCommand("my-project", "uptime")
 
 // run the "uptime" command on all unix nodes
@@ -103,7 +103,7 @@ import org.rundeck.api.NodeFiltersBuilder
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-// trigger the execution of a custom bash script on the RunDeck server
+// trigger the execution of a custom bash script on the Rundeck server
 execution = rundeck.triggerAdhocScript("my-project", "/tmp/my-script.sh")
 
 // run a bash script (with options) on all unix nodes
@@ -132,5 +132,5 @@ puts "#{result.succeededJobs.size} jobs successfully imported, #{result.skippedJ
 
 h2. And more...
 
-See the API documentation of the [RundeckClient|./apidocs/reference/org/rundeck/api/RundeckClient.html] class for more interactions with your RunDeck instance...
+See the API documentation of the [RundeckClient|./apidocs/reference/org/rundeck/api/RundeckClient.html] class for more interactions with your Rundeck instance...
 
diff --git a/src/site/confluence/jython.confluence b/src/site/confluence/jython.confluence
index 88019aa..8e8bb0d 100644
--- a/src/site/confluence/jython.confluence
+++ b/src/site/confluence/jython.confluence
@@ -1,5 +1,5 @@
 
-h1. Using the RunDeck API from Jython scripts
+h1. Using the Rundeck API from Jython scripts
 
 Here are some examples of what you can do with this lib and a few lines of [Python|http://www.jython.org/] (for the pythonist that don't fear running on the JVM !)
 
@@ -14,18 +14,18 @@ from org.rundeck.api import RundeckClient
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-print("RunDeck uptime : %s" % rundeck.systemInfo.uptime)
-print("All RunDeck projects : %s" % rundeck.projects)
-print("All RunDeck nodes : %s" % rundeck.nodes)
-print("All RunDeck jobs : %s" % rundeck.jobs)
-print("All RunDeck running executions : %s" % rundeck.runningExecutions)
+print("Rundeck uptime : %s" % rundeck.systemInfo.uptime)
+print("All Rundeck projects : %s" % rundeck.projects)
+print("All Rundeck nodes : %s" % rundeck.nodes)
+print("All Rundeck jobs : %s" % rundeck.jobs)
+print("All Rundeck running executions : %s" % rundeck.runningExecutions)
 {code}
 
 h2. Authentication
 
-Starting with RunDeck API 2, there are 2 ways to authenticate :
+Starting with Rundeck API 2, there are 2 ways to authenticate :
 * the *login-based authentication* : with your login and password
-* the *token-based authentication* : with a unique token that you can generate from the RunDeck webUI
+* the *token-based authentication* : with a unique token that you can generate from the Rundeck webUI
 
 {code}
 // using login-based authentication (admin/admin is the default login/password for a new Rundeck instance) :
@@ -71,7 +71,7 @@ from org.rundeck.api import NodeFiltersBuilder
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-// trigger the execution of the "uptime" command on the RunDeck server
+// trigger the execution of the "uptime" command on the Rundeck server
 execution = rundeck.triggerAdhocCommand("my-project", "uptime")
 
 // run the "uptime" command on all unix nodes
@@ -87,7 +87,7 @@ from org.rundeck.api import NodeFiltersBuilder
 
 rundeck = RundeckClient.builder().url("http://localhost:4440").login("admin", "admin").build()
 
-// trigger the execution of a custom bash script on the RunDeck server
+// trigger the execution of a custom bash script on the Rundeck server
 execution = rundeck.triggerAdhocScript("my-project", "/tmp/my-script.sh")
 
 // run a bash script (with options) on all unix nodes
@@ -116,5 +116,5 @@ print("%s jobs successfully imported, %s jobs skipped, and %s jobs failed" % (re
 
 h2. And more...
 
-See the API documentation of the [RundeckClient|./apidocs/reference/org/rundeck/api/RundeckClient.html] class for more interactions with your RunDeck instance...
+See the API documentation of the [RundeckClient|./apidocs/reference/org/rundeck/api/RundeckClient.html] class for more interactions with your Rundeck instance...
 
diff --git a/src/site/confluence/status.confluence b/src/site/confluence/status.confluence
index 343131c..df82d52 100644
--- a/src/site/confluence/status.confluence
+++ b/src/site/confluence/status.confluence
@@ -1,9 +1,9 @@
 
-h1. Status of the implementation of the RunDeck API
+h1. Status of the implementation of the Rundeck API
 
-h2. RunDeck API version 1
+h2. Rundeck API version 1
 
-[Documentation of the RunDeck API version 1|http://rundeck.org/1.2.1/RunDeck-Guide.html#rundeck-api]
+[Documentation of the Rundeck API version 1|http://rundeck.org/1.2.1/Rundeck-Guide.html#rundeck-api]
 
 * Login-based authentication - OK
 * System Info - OK
@@ -26,67 +26,67 @@ h2. RunDeck API version 1
 * Listing Resources - OK
 * Getting Resource Info - OK
 
-h2. RunDeck API version 2
+h2. Rundeck API version 2
 
-[Documentation of the RunDeck API version 2|http://rundeck.org/1.3.2/api/index.html]
+[Documentation of the Rundeck API version 2|http://rundeck.org/1.3.2/api/index.html]
 
 * Token-based authentication - OK
 * Listing Jobs for a Project - *TODO*
 * Updating and Listing Resources for a Project - *TODO*
 * Refreshing Resources for a Project - *TODO*
 
-h2. RunDeck API version 3
+h2. Rundeck API version 3
 
-[Documentation of the RunDeck API version 3|http://rundeck.org/1.4.2/api/index.html]
+[Documentation of the Rundeck API version 3|http://rundeck.org/1.4.2/api/index.html]
 
 * (only updates to Resource endpoints) - *TODO*
 
-h2. RunDeck API version 4
+h2. Rundeck API version 4
 
-[Documentation of the RunDeck API version 4|http://rundeck.org/1.4.3/api/index.html]
+[Documentation of the Rundeck API version 4|http://rundeck.org/1.4.3/api/index.html]
 
 * Running Adhoc Script URLs - *TODO*
 
-h2. RunDeck API version 5
+h2. Rundeck API version 5
 
-[Documentation of the RunDeck API version 5|http://rundeck.org/1.4.4/api/index.html]
+[Documentation of the Rundeck API version 5|http://rundeck.org/1.4.4/api/index.html]
 
 * Bulk Job Delete - OK
 * Execution Output - OK
 * Execution Query - OK
 * History list query - OK
 
-h2. RunDeck API version 6
+h2. Rundeck API version 6
 
-[Documentation of the RunDeck API version 6|http://rundeck.org/1.5/api/index.html]
+[Documentation of the Rundeck API version 6|http://rundeck.org/1.5/api/index.html]
 
 * Execution Output format fixed - OK
 
-h2. RunDeck API version 7
+h2. Rundeck API version 7
 
-[Documentation of the RunDeck API version 7|http://rundeck.org/1.5.3/api/index.html]
+[Documentation of the Rundeck API version 7|http://rundeck.org/1.5.3/api/index.html]
 
 * Incubator for cluster mode job takeover - *TODO*
 
-h2. RunDeck API version 8
+h2. Rundeck API version 8
 
-[Documentation of the RunDeck API version 8|http://rundeck.org/1.6.0/api/index.html]
+[Documentation of the Rundeck API version 8|http://rundeck.org/1.6.0/api/index.html]
 
 * scriptInterpreter addition to run script and run url - OK
 * project parameter added to jobs import - OK
 
 
-h2. RunDeck API version 9
+h2. Rundeck API version 9
 
-[Documentation of the RunDeck API version 9|http://rundeck.org/1.6.1/api/index.html]
+[Documentation of the Rundeck API version 9|http://rundeck.org/1.6.1/api/index.html]
 
 * list running executions across all projects - OK
 * include project name in execution results - OK
 * Add uuidOption parameter to allow removing imported UUIDs to avoid creation conflicts - OK
 
-h2. RunDeck API version 10
+h2. Rundeck API version 10
 
-[Documentation of the RunDeck API version 10|http://rundeck.org/2.0.0/api/index.html]
+[Documentation of the Rundeck API version 10|http://rundeck.org/2.0.0/api/index.html]
 
 * Execution State - Retrieve workflow step and node state information - OK
 * Execution Output with State - Retrieve log output with state change information - OK
@@ -95,9 +95,9 @@ h2. RunDeck API version 10
 * Deprecation: Remove methods deprecated until version 10. - OK
 
 
-h2. RunDeck API version 11
+h2. Rundeck API version 11
 
-[Documentation of the RunDeck API version 11|http://rundeck.org/2.1.0/api/index.html]
+[Documentation of the Rundeck API version 11|http://rundeck.org/2.1.0/api/index.html]
 
 * Project creation - OK
 * Get Project configuration - OK
@@ -116,7 +116,7 @@ h2. RunDeck API version 11
 
 h2. Rundeck API version 12
 
-[Documentation of the RunDeck API version 12|http://rundeck.org/2.2.0/api/index.html]
+[Documentation of the Rundeck API version 12|http://rundeck.org/2.2.0/api/index.html]
 
 * Bulk delete executions - OK
 * Delete execution - OK
diff --git a/src/site/site.xml b/src/site/site.xml
index 2e2843a..db7f566 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -28,8 +28,8 @@
   
   
     
-      
-      
+      
+      
     
     
       
@@ -46,7 +46,7 @@
       
     
     
     
   

From a299c12c39cfa6d273f06eb63376661aa50ae4ef Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Tue, 25 Nov 2014 11:04:25 -0800
Subject: [PATCH 88/89] [maven-release-plugin] prepare release
 rundeck-api-java-client-12.0

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 15770f2..77fe8b5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  12.0-SNAPSHOT
+  12.0
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API

From 4d49ef39bd0dd4812d95e75d3b1282c21ebeb9f4 Mon Sep 17 00:00:00 2001
From: Greg Schueler 
Date: Tue, 25 Nov 2014 11:04:29 -0800
Subject: [PATCH 89/89] [maven-release-plugin] prepare for next development
 iteration

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 77fe8b5..813f635 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
   
   org.rundeck
   rundeck-api-java-client
-  12.0
+  12.1-SNAPSHOT
   jar
   RunDeck API - Java Client
   Java client for the RunDeck REST API