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 diff --git a/dependencies-check-rules.xml b/dependencies-check-rules.xml new file mode 100644 index 0000000..ead5a09 --- /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 7611fe3..813f635 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,10 @@ 4.0.0 + + 2.2.1 + + org.sonatype.oss @@ -27,7 +31,7 @@ org.rundeck rundeck-api-java-client - 9.3 + 12.1-SNAPSHOT jar RunDeck API - Java Client Java client for the RunDeck REST API @@ -88,6 +92,16 @@ 1.6 UTF-8 + + 4.3.6 + 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 diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 7a679ae..98deb4a 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -19,9 +19,59 @@ 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 + + + + 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 + + + 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/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 8d621bb..da5c5a6 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -15,23 +15,18 @@ */ package org.rundeck.api; +import org.apache.commons.io.IOUtils; 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; +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; @@ -42,17 +37,14 @@ 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; 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; @@ -66,7 +58,7 @@ import java.util.Map.Entry; /** * Class responsible for making the HTTP API calls - * + * * @author Vincent Behar */ class ApiCall { @@ -79,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 */ @@ -94,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 { @@ -115,7 +107,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() @@ -133,7 +126,7 @@ class ApiCall { /** * Test the login-based authentication on the RunDeck instance - * + * * @throws RundeckApiLoginException if the login fails * @see #testAuth() */ @@ -150,7 +143,7 @@ class ApiCall { /** * Test the token-based authentication on the RunDeck instance - * + * * @throws RundeckApiTokenException if the token is invalid * @see #testAuth() */ @@ -167,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 @@ -177,13 +170,17 @@ 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); } /** * 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 @@ -192,7 +189,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); @@ -204,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 @@ -213,7 +214,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; @@ -242,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 @@ -257,7 +262,30 @@ 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()); + } // POST a multi-part request, with all attachments if(apiPath.getAttachments().size()>0){ MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); @@ -271,6 +299,18 @@ 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()))); + }else if(apiPath.isEmptyContent()){ + //empty content }else { throw new IllegalArgumentException("No Form or Multipart entity for POST content-body"); } @@ -281,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 @@ -293,11 +333,27 @@ 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, * 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 @@ -308,23 +364,171 @@ 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()); + } + 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; + } /** * 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 ByteArrayInputStream execute(HttpRequestBase request) throws RundeckApiException, RundeckApiLoginException, + 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 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 + */ + 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 { @@ -345,7 +549,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()); @@ -355,33 +560,31 @@ 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()); } - - // 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(); } @@ -390,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 */ @@ -435,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) { @@ -459,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()); } @@ -485,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() { diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java index 0fe823c..7ef79ee 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; @@ -26,6 +27,8 @@ 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.generator.XmlDocumentGenerator; import org.rundeck.api.util.ParametersUtil; /** @@ -38,9 +41,17 @@ 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(); + private Document xmlDocument; + 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 "&") */ private boolean firstParamDone = false; @@ -54,6 +65,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)) { @@ -61,8 +76,16 @@ class ApiPathBuilder { } } } + return this; } + /** + * Set the accept header + */ + public ApiPathBuilder accept(String mimeTypes) { + accept=mimeTypes; + return this; + } /** * Visit a {@link BuildsParameters} and add the parameters */ @@ -257,6 +280,69 @@ 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, 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. + * + * @param document XMl document to send + * @return this, for method chaining + */ + public ApiPathBuilder xml(final Document document) { + if (document != null) { + xmlDocument = document; + } + 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 @@ -302,7 +388,43 @@ 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; + } + + /** + * Accept header value, default "text/xml" + */ + public String getAccept() { + return accept; + } + + public Document getXmlDocument() { + return xmlDocument; + } + + public InputStream getContentStream() { + return contentStream; + } + + public String getContentType() { + return contentType; + } + + public File getContentFile() { + return contentFile; + } + + public boolean isEmptyContent() { + return emptyContent; + } + + public ApiPathBuilder requireContentType(String contentType) { + this.requiredContentType=contentType; + return this; + } + + public String getRequiredContentType() { + return requiredContentType; } /** 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 diff --git a/src/main/java/org/rundeck/api/RundeckApiException.java b/src/main/java/org/rundeck/api/RundeckApiException.java index 141e905..5a2de8b 100644 --- a/src/main/java/org/rundeck/api/RundeckApiException.java +++ b/src/main/java/org/rundeck/api/RundeckApiException.java @@ -82,5 +82,65 @@ 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; + } + } + + /** + * 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 b218702..fb2ef09 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -18,59 +18,50 @@ 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.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; import org.rundeck.api.parser.*; import org.rundeck.api.query.ExecutionQuery; 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.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; +import java.io.*; +import java.util.*; import java.util.concurrent.TimeUnit; /** - * Main entry point to talk to a RunDeck instance. + * Rundeck API client. *

- * 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 + * 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();
  *
@@ -91,6 +82,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 STORAGE_KEYS_PATH = "keys/";
 
     /**
      * Supported version numbers
@@ -101,6 +94,9 @@ public class RundeckClient implements Serializable {
         V7(7),
         V8(8),
         V9(9),
+        V10(10),
+        V11(11),
+        V12(12),
         ;
 
         private int versionNumber;
@@ -114,7 +110,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.V12.getVersionNumber();
 
     private static final String API = "/api/";
 
@@ -122,10 +118,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;
@@ -180,6 +176,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);
@@ -199,9 +198,9 @@ 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.
      */
-    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){
@@ -219,6 +218,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);
     }
@@ -259,7 +268,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
@@ -267,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
      *
@@ -282,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"));
     }
 
     /**
@@ -299,7 +325,274 @@ 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 create a project !");
+        return new ApiCall(this)
+                .post(new ApiPathBuilder("/projects").xml(
+                        projectDocument(projectName, configuration)
+                ), createProjectParser(rootXpath() +
+                        (getApiVersion() < Version.V11.getVersionNumber()
+                                ? "/projects/project"
+                                : "/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));
+    }
+    /**
+     * 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);
+    }
+
+    /**
+     * 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
+     *
+     * @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"));
+    }
+    /**
+     * 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
+     *
+     * @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();
+        project.setName(projectName);
+        if (null != configuration) {
+            project.setProjectConfig(new ProjectConfig(configuration));
+        }
+        return new ProjectGenerator(project).generateXmlDocument();
     }
 
     /*
@@ -359,7 +652,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"));
     }
 
     /**
@@ -541,11 +834,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, ",")));
     }
 
     /**
@@ -632,193 +927,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
@@ -877,7 +985,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()));
     }
 
     /**
@@ -933,7 +1041,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("result/success/message"));
+        new ApiCall(this).delete(new ApiPathBuilder("/job/", jobId));
+        return "Job " + jobId + " was deleted successfully";
     }
     /**
      * Delete multiple jobs, identified by the given IDs
@@ -951,96 +1060,9 @@ 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"));
     }
 
-    /**
-     * 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)
@@ -1063,75 +1085,18 @@ 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,
+                                     APIV11Helper.unwrapIfNeeded(
+                                             new ExecutionParser(
+                                                     rootXpath() +
+                                                     "/executions/execution"
+                                             ), rootXpath() +
+                                                "/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.
@@ -1153,87 +1118,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.
@@ -1280,101 +1164,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.
@@ -1402,139 +1192,11 @@ 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());
     }
 
-    /**
-     * 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
@@ -1557,70 +1219,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
@@ -1663,323 +1261,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
@@ -2045,445 +1326,11 @@ 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());
     }
 
-    /**
-     * 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
@@ -2507,80 +1354,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
@@ -2700,7 +1473,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"));
     }
 
     /**
@@ -2798,7 +1571,7 @@ public class RundeckClient implements Serializable {
                                          .param("max", max)
                                          .param("offset", offset),
                                      new ListParser(new ExecutionParser(),
-                                                                      "result/executions/execution"));
+                                                                      rootXpath()+"/executions/execution"));
     }
 
     /**
@@ -2823,9 +1596,15 @@ public class RundeckClient implements Serializable {
                                          .param(new ExecutionQueryParameters(query))
                                          .param("max", max)
                                          .param("offset", offset),
-                                     new PagedResultParser(
-                                         new ListParser(new ExecutionParser(), "execution"),
-                                         "result/executions"
+                                     APIV11Helper.unwrapIfNeeded(
+                                             new PagedResultParser(
+                                                     new ListParser(
+                                                             new ExecutionParser(),
+                                                             "execution"
+                                                     ),
+                                                     rootXpath() + "/executions"
+                                             ),
+                                             rootXpath() + "/executions"
                                      )
         );
     }
@@ -2843,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("result/executions/execution"));
+        return new ApiCall(this).get(
+                new ApiPathBuilder("/execution/", executionId.toString()),
+                APIV11Helper.unwrapIfNeeded(
+                        new ExecutionParser(
+                                rootXpath() + "/executions/execution"
+                        ),
+                        rootXpath() + "/executions/execution"
+                )
+        );
     }
 
     /**
@@ -2879,7 +1665,76 @@ 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"));
+    }
+
+    /**
+     * 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")
+        );
+    }
+
+    /**
+     * Delete a set of executions, identified by the given IDs
+     *
+     * @param executionIds set of identifiers for the executions - mandatory
+     * @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 deleteExecutions(final Set executionIds)
+            throws RundeckApiException, RundeckApiLoginException,
+                   RundeckApiTokenException, IllegalArgumentException
+    {
+        AssertUtil.notNull(executionIds, "executionIds is mandatory to delete executions!");
+        if (executionIds.size() < 1) {
+            throw new IllegalArgumentException("executionIds cannot be empty");
+        }
+        final ApiPathBuilder apiPath = new ApiPathBuilder("/executions/delete").xml(
+                new DeleteExecutionsGenerator(executionIds)
+        );
+        return new ApiCall(this).post(
+                apiPath,
+                new DeleteExecutionsResponseParser( rootXpath() + "/deleteExecutions")
+        );
+    }
+
+    /**
+     * 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 delete an execution!");
+        new ApiCall(this).delete(new ApiPathBuilder("/execution/", executionId.toString()));
     }
 
     /*
@@ -3071,7 +1926,7 @@ public class RundeckClient implements Serializable {
                                          .param("end", end)
                                          .param("max", max)
                                          .param("offset", offset),
-                                     new HistoryParser("result/events"));
+                                     new HistoryParser(rootXpath()+"/events"));
     }
 
     /**
@@ -3118,7 +1973,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"));
     }
 
     /*
@@ -3258,15 +2113,191 @@ 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)}, 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 {
+        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!");
-        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()));
+        ApiPathBuilder param = new ApiPathBuilder(
+                "/execution/", executionId.toString(),
+                "/output")
+                .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(rootXpath()+"/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(rootXpath()+"/executionState"));
+    }
+
+    /**
+     * 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 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!");
+        AssertUtil.notNull(nodeName, "nodeName is mandatory to get the output of a job execution!");
+        ApiPathBuilder param = new ApiPathBuilder(
+                "/execution/", executionId.toString(),
+                "/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(rootXpath()+"/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 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!");
+        AssertUtil.notNull(stepCtx, "stepCtx is mandatory to get the output of a job execution!");
+        ApiPathBuilder param = new ApiPathBuilder(
+                "/execution/", executionId.toString(),
+                "/output/step/", stepCtx)
+                .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(rootXpath()+"/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 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!");
+        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!");
+        ApiPathBuilder param = new ApiPathBuilder(
+                "/execution/", executionId.toString(),
+                "/output/node/", nodeName,
+                "/step/", stepCtx)
+                .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(rootXpath()+"/output", createOutputEntryParser()));
     }
 
 
@@ -3282,14 +2313,69 @@ 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)}, 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 {
+        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!");
-        return new ApiCall(this).get(new ApiPathBuilder("/execution/", executionId.toString(), "/output.xml").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(rootXpath()+"/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 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")
+                .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(rootXpath()+"/output", createOutputEntryParser()));
     }
 
     private OutputEntryParser createOutputEntryParser() {
@@ -3315,7 +2401,204 @@ 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"));
+    }
+
+
+    /*
+     * 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"));
+    }
+
+    /**
+     * 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 key resource
+     * @throws RundeckApiException
+     */
+    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(
+                        privateKey ? "application/octet-stream" : "application/pgp-keys",
+                        keyfile
+                ),
+                new SSHKeyResourceParser("/resource")
+        );
+    }
+
+    /**
+     * Get metadata for an key file
+     *
+     * @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 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);
+        }
+        KeyResource storageResource = new ApiCall(this).get(
+                new ApiPathBuilder(STORAGE_ROOT_PATH, path),
+                new SSHKeyResourceParser("/resource")
+        );
+        if (storageResource.isDirectory()) {
+            throw new RundeckApiException("Key Path is a directory: " + path);
+        }
+        return storageResource;
+    }
+
+    /**
+     * 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 getPublicKeyContent(final String path, final OutputStream out) throws
+            RundeckApiException, IOException {
+        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(
+                    new ApiPathBuilder(STORAGE_ROOT_PATH, path)
+                            .accept("application/pgp-keys")
+                            .requireContentType("application/pgp-keys"),
+                    out
+            );
+        } catch (RundeckApiException.RundeckApiHttpContentTypeException e) {
+            throw new RundeckApiException("Requested Key path was not a Public key: " + path, e);
+        }
+    }
+
+    /**
+     * 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 getPublicKeyContent(final String path, final File out) throws
+            RundeckApiException, IOException {
+        final FileOutputStream fileOutputStream = new FileOutputStream(out);
+        try {
+            return getPublicKeyContent(path, fileOutputStream);
+        }finally {
+            fileOutputStream.close();
+        }
+    }
+
+    /**
+     * List contents of root key directory
+     *
+     * @return list of key resources
+     * @throws RundeckApiException
+     */
+    public List listKeyDirectoryRoot() throws RundeckApiException {
+        return listKeyDirectory(STORAGE_KEYS_PATH);
+    }
+    /**
+     * List contents of key directory
+     *
+     * @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 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);
+        }
+        KeyResource storageResource = new ApiCall(this).get(
+                new ApiPathBuilder(STORAGE_ROOT_PATH, path),
+                new SSHKeyResourceParser("/resource")
+        );
+        if(!storageResource.isDirectory()) {
+            throw new RundeckApiException("key path is not a directory path: " + path);
+        }
+        return storageResource.getDirectoryContents();
+    }
+
+    /**
+     * Delete an key file
+     * @param path a path to a key file, must start with "keys/"
+     */
+    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/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/domain/BaseKeyResource.java b/src/main/java/org/rundeck/api/domain/BaseKeyResource.java
new file mode 100644
index 0000000..c1ed3b7
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/BaseKeyResource.java
@@ -0,0 +1,58 @@
+package org.rundeck.api.domain;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * BaseKeyResource is ...
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public class BaseKeyResource extends BaseStorageResource implements KeyResource {
+    private boolean privateKey;
+
+    public BaseKeyResource() {
+    }
+
+
+    public boolean isPrivateKey() {
+        return privateKey;
+    }
+
+    public void setPrivateKey(boolean privateKey) {
+        this.privateKey = privateKey;
+    }
+
+    ArrayList keyResources = new ArrayList();
+
+    @Override
+    public void setDirectoryContents(List directoryContents) {
+        for (StorageResource directoryContent : directoryContents) {
+            keyResources.add(from(directoryContent));
+        }
+    }
+
+    @Override
+    public List getDirectoryContents() {
+        return keyResources;
+    }
+
+    public static BaseKeyResource from(final StorageResource source) {
+        final BaseKeyResource baseSshKeyResource = new BaseKeyResource();
+        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-key-type"))
+            );
+        } else if (null != source.getDirectoryContents()) {
+            baseSshKeyResource.setDirectoryContents(source.getDirectoryContents());
+        }
+        return baseSshKeyResource;
+    }
+}
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..f97fa23
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/BaseState.java
@@ -0,0 +1,63 @@
+package org.rundeck.api.domain;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base execution status for a step
+ */
+public class BaseState {
+    private Date startTime;
+    private Date endTime;
+    private Date updateTime;
+    private RundeckWFExecState executionState;
+
+    /**
+     * Time that the execution of this step started
+     * @return
+     */
+    public Date getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(Date startTime) {
+        this.startTime = startTime;
+    }
+
+    /**
+     * Time that the execution of this step finished, or null if it has not completed
+     * @return
+     */
+    public Date getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(Date endTime) {
+        this.endTime = endTime;
+    }
+
+    /**
+     * Current state of the execution
+     * @return
+     */
+    public RundeckWFExecState getExecutionState() {
+        return executionState;
+    }
+
+    public void setExecutionState(RundeckWFExecState executionState) {
+        this.executionState = executionState;
+    }
+
+    /**
+     * Time that this state was last updated
+     * @return
+     */
+    public Date getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(Date updateTime) {
+        this.updateTime = updateTime;
+    }
+}
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/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/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/domain/KeyResource.java b/src/main/java/org/rundeck/api/domain/KeyResource.java
new file mode 100644
index 0000000..ff1f7e1
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/KeyResource.java
@@ -0,0 +1,23 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+
+/**
+ * KeyResource represents a directory or an SSH key file
+ *
+ * @author Greg Schueler 
+ * @since 2014-04-04
+ */
+public interface KeyResource 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/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/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/RundeckExecutionState.java b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java
new file mode 100644
index 0000000..c06a4bf
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/RundeckExecutionState.java
@@ -0,0 +1,50 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 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 targeted in this execution
+     * @return
+     */
+    public Set getAllNodes() {
+        return allNodes;
+    }
+
+    public void setAllNodes(Set allNodes) {
+        this.allNodes = allNodes;
+    }
+
+    /**
+     * Return the map of node name to step state list
+     * @return
+     */
+    public Map> getNodeStates() {
+        return nodeStates;
+    }
+
+    public void setNodeStates(Map> nodeStates) {
+        this.nodeStates = nodeStates;
+    }
+
+    /**
+     * Return this execution's ID
+     * @return
+     */
+    public long getExecutionId() {
+        return executionId;
+    }
+
+    public void setExecutionId(long executionId) {
+        this.executionId = executionId;
+    }
+}
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/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/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/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/domain/RundeckWFExecState.java b/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java
new file mode 100644
index 0000000..86b0466
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/RundeckWFExecState.java
@@ -0,0 +1,43 @@
+package org.rundeck.api.domain;
+
+/**
+ * An execution state for a workflow or node step
+ */
+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/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/domain/WorkflowState.java b/src/main/java/org/rundeck/api/domain/WorkflowState.java
new file mode 100644
index 0000000..3ce17a2
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/WorkflowState.java
@@ -0,0 +1,52 @@
+package org.rundeck.api.domain;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Represents the state of a workflow of steps
+ */
+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;
+    }
+
+    public void setStepCount(int stepCount) {
+        this.stepCount = stepCount;
+    }
+
+    /**
+     * Identify the target nodes of this workflow
+     * @return
+     */
+    public Set getTargetNodes() {
+        return targetNodes;
+    }
+
+    public void setTargetNodes(Set targetNodes) {
+        this.targetNodes = targetNodes;
+    }
+
+    /**
+     * Return the list of steps for this workflow
+     * @return
+     */
+    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..ef6e80c
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/WorkflowStepContextState.java
@@ -0,0 +1,33 @@
+package org.rundeck.api.domain;
+
+/**
+ * A state for a particular step
+ */
+public class WorkflowStepContextState extends BaseState {
+    private String stepContextId;
+    private int stepNum;
+
+    /**
+     * The context id for the step in the form "#[/#[/#[...]]]" where "#" is a number
+     * @return
+     */
+    public String getStepContextId() {
+        return stepContextId;
+    }
+
+    public void setStepContextId(String stepContextId) {
+        this.stepContextId = stepContextId;
+    }
+
+    /**
+     * The step number of this step in the current workflow, 1 indexed.
+     * @return
+     */
+    public int getStepNum() {
+        return stepNum;
+    }
+
+    public void setStepNum(int 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..49a8d93
--- /dev/null
+++ b/src/main/java/org/rundeck/api/domain/WorkflowStepState.java
@@ -0,0 +1,49 @@
+package org.rundeck.api.domain;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 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;
+    }
+
+    public void setNodeStep(boolean nodeStep) {
+        this.nodeStep = nodeStep;
+    }
+
+    /**
+     * Return sub workflow if this step has one
+     * @return
+     */
+    public WorkflowState getSubWorkflow() {
+        return subWorkflow;
+    }
+
+    public void setSubWorkflow(WorkflowState subWorkflow) {
+        this.subWorkflow = subWorkflow;
+    }
+
+    /**
+     * Return the state of each target node if this step runs on each target node
+     * @return
+     */
+    public Map getNodeStates() {
+        return nodeStates;
+    }
+
+    public void setNodeStates(Map nodeStates) {
+        this.nodeStates = nodeStates;
+    }
+}
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/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/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/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/generator/ProjectGenerator.java b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
new file mode 100644
index 0000000..8960acf
--- /dev/null
+++ b/src/main/java/org/rundeck/api/generator/ProjectGenerator.java
@@ -0,0 +1,32 @@
+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 extends BaseDocGenerator {
+    RundeckProject project;
+
+    public ProjectGenerator(RundeckProject project) {
+        this.project = project;
+    }
+
+    @Override
+    public Element generateXmlElement() {
+        Element rootElem = DocumentFactory.getInstance().createElement("project");
+        rootElem.addElement("name").setText(project.getName());
+        ProjectConfig configuration = project.getProjectConfig();
+        if (null != configuration) {
+            rootElem.add(new ProjectConfigGenerator(configuration).generateXmlElement());
+        }
+        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/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/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/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/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/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/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/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/OutputEntryParser.java b/src/main/java/org/rundeck/api/parser/OutputEntryParser.java
index 9754377..c5268dc 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,44 @@ 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);
+            return parse;
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
     /**
      * Parse the message content
      */
@@ -54,4 +100,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/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/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/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/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/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/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
new file mode 100644
index 0000000..091ab2b
--- /dev/null
+++ b/src/main/java/org/rundeck/api/parser/SSHKeyResourceParser.java
@@ -0,0 +1,25 @@
+package org.rundeck.api.parser;
+
+import org.dom4j.Node;
+import org.rundeck.api.domain.BaseKeyResource;
+import org.rundeck.api.domain.KeyResource;
+
+/**
+ * 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 KeyResource parse(Node node) {
+        return BaseKeyResource.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/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..55b6386
--- /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(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
+            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/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();
+    }
+}
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)
diff --git a/src/site/confluence/groovy.confluence b/src/site/confluence/groovy.confluence
index 6dc06e5..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/].
 
@@ -11,16 +11,16 @@ 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}"
-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}}".
@@ -28,34 +28,34 @@ 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}"
-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) :
-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,9 +80,9 @@ 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
+// 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
@@ -93,9 +93,9 @@ 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
+// 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
@@ -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"
@@ -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 cedb4ae..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 !)
 
@@ -14,13 +14,13 @@ 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}"
-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}}".
@@ -28,27 +28,27 @@ 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}"
-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) :
-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,9 +85,9 @@ 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
+// 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
@@ -101,9 +101,9 @@ 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
+// 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
@@ -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"
@@ -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 b2dbc94..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 !)
 
@@ -12,27 +12,27 @@ 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)
-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) :
-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,9 +69,9 @@ 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
+// 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
@@ -85,9 +85,9 @@ 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
+// 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
@@ -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))
@@ -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 4807a5c..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,60 +26,98 @@ 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
+
+[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
+* 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
+* 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
+
+h2. Rundeck API version 12
+
+[Documentation of the Rundeck API version 12|http://rundeck.org/2.2.0/api/index.html]
+
+* Bulk delete executions - OK
+* Delete execution - OK
+* Delete all executions for a job - 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 @@
       
     
     
     
   
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());
+    }
+}
diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java
index c5330be..04211f6 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,8 +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.InputStream;
+import java.io.*;
 import java.util.*;
 
 
@@ -55,6 +55,8 @@ 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";
+    public static final String TEST_TOKEN_8 = "GG7uj1y6UGahOs7QlmeN2sIwz1Y2j7zI";
 
     @Rule
     public Recorder recorder = new Recorder();
@@ -82,6 +84,131 @@ 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 = "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
+    @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 = "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_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 = "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 = "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")
     public void getHistory() throws Exception {
         final RundeckHistory test = client.getHistory("test");
@@ -148,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,
@@ -275,6 +442,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
@@ -364,22 +657,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 +674,49 @@ 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_as_user")
-    public void triggerJobDeprecatedAsUser() throws Exception {
-        RundeckClient client = createClient(TEST_TOKEN_3, 5);
+    @Betamax(tape = "trigger_job_basic_v11")
+    public void triggerJobBasic_v11() throws Exception {
+        RundeckClient client = createClient(TEST_TOKEN_7, 11);
 
         final RundeckExecution test
-            = client.triggerJob("3170ba0e-6093-4b58-94d2-52988aefbfc9", null, null, "api-java-client-user-test1");
+            = client.triggerJob(RunJobBuilder.builder().setJobId("bda8b956-43a5-4eef-9c67" +
+                                                                 "-3f27cc0ee1a5").build());
 
-        Assert.assertEquals((Long) 20L, test.getId());
+        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("api-java-client-user-test1", test.getStartedBy());
+        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")
     public void triggerJobAsUser() throws Exception {
@@ -431,19 +736,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 +754,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")
@@ -497,20 +776,43 @@ public class RundeckClientTest {
     }
 
     @Test
-    @Betamax(tape = "trigger_adhoc_command_as_user")
-    public void triggerAdhocCommandDeprecatedAsUser() throws Exception {
-        RundeckClient client = createClient(TEST_TOKEN_3, 5);
+    @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("test", "echo test trigger_adhoc_command_as_user",null,null,null,"api-java-client-test-run-command-as-user1");
+                = client.triggerAdhocCommand(RunAdhocCommandBuilder.builder()
+                .setProject("test")
+                .setCommand("echo test trigger_adhoc_command")
+                .build());
 
-        Assert.assertEquals((Long) 24L, test.getId());
+        Assert.assertEquals((Long) 945L, 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());
+        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")
     public void triggerAdhocCommandAsUser() throws Exception {
@@ -532,19 +834,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 +855,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 +875,55 @@ public class RundeckClientTest {
         Assert.assertEquals("admin", test.getStartedBy());
         Assert.assertEquals(RundeckExecution.ExecutionStatus.RUNNING, test.getStatus());
     }
+
+
+    /**
+     * Handle incorrect <result> wrapper for v11 response
+     * @throws Exception
+     */
     @Test
-    @Betamax(tape = "trigger_adhoc_script_as_user")
-    public void triggerAdhocScriptDeprecatedAsUser() throws Exception {
-        RundeckClient client = createClient(TEST_TOKEN_3, 5);
+    @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("test", byteArrayInputStream, (Properties) null, null, null, null, "api-java-client-test-adhoc-script-as-user1");
+                = client.triggerAdhocScript(RunAdhocScriptBuilder.builder().setProject("test").setScript
+                (byteArrayInputStream).build());
 
-        Assert.assertEquals((Long) 26L, test.getId());
+        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("api-java-client-test-adhoc-script-as-user1", test.getStartedBy());
+        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 {
@@ -639,23 +943,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 {
@@ -1034,7 +1322,647 @@ 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.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());
+        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.getExecutionOutputForStep(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.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());
+        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());
+    }
+
+    /**
+     * 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.getExecutionOutputState(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.getExecutionOutputState(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());
+    }
+    /**
+     * 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());
+    }
+
+    /**
+     * 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());
+        }
+    }
+    /**
+     * Store ssh key
+     */
+    @Test
+    @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();
+        FileOutputStream out = new FileOutputStream(temp);
+        try{
+            out.write("test1".getBytes());
+        }finally {
+            out.close();
+        }
+        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("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-key-type"));
+    }
+    /**
+     * Store ssh key
+     */
+    @Test
+    @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();
+        FileOutputStream out = new FileOutputStream(temp);
+        try{
+            out.write("test1".getBytes());
+        }finally {
+            out.close();
+        }
+        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("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-key-type"));
+    }
+    /**
+     * get ssh key
+     */
+    @Test
+    @Betamax(tape = "key_get_public", mode = TapeMode.READ_ONLY)
+    public void getKey_public() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        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("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-key-type"));
+    }
+    /**
+     * get ssh key
+     */
+    @Test
+    @Betamax(tape = "key_get_private", mode = TapeMode.READ_ONLY)
+    public void getKey_private() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        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("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-key-type"));
+    }
+    /**
+     * get ssh key data
+     */
+    @Test
+    @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.getPublicKeyContent("keys/test/example/file1.pem", temp);
+            Assert.fail("expected failure");
+        } catch (RundeckApiException e) {
+            Assert.assertEquals("Requested Key path was not a Public key: keys/test/example/file1.pem",
+                    e.getMessage());
+        }
+    }
+    /**
+     * get ssh key data
+     */
+    @Test
+    @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.getPublicKeyContent("keys/test/example/file2.pub", temp);
+        Assert.assertEquals(5, length);
+    }
+    /**
+     * list directory
+     */
+    @Test
+    @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.listKeyDirectory("keys/test/example");
+        Assert.assertEquals(2, list.size());
+        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("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-key-type"));
+
+        Assert.assertFalse(storageResource1.isDirectory());
+        Assert.assertFalse(storageResource1.isPrivateKey());
+        Assert.assertEquals("file2.pub", storageResource1.getName());
+        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-key-type"));
+    }
+    /**
+     * list root
+     */
+    @Test
+    @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.listKeyDirectoryRoot();
+        Assert.assertEquals(2, list.size());
+        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("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-key-type"));
+
+        Assert.assertTrue(storageResource1.toString(), storageResource1.isDirectory());
+        Assert.assertEquals(null, storageResource1.getName());
+        Assert.assertEquals("keys/test", storageResource1.getPath());
+        Assert.assertEquals("http://dignan.local:4440/api/11/storage/keys/test",
+                storageResource1.getUrl());
+        Assert.assertNull(storageResource1.getMetadata());
+
+
+    }
+
+    /**
+     * delete ssh key
+     */
+    @Test
+    @Betamax(tape = "key_delete", mode = TapeMode.READ_ONLY)
+    public void deleteKey() throws Exception {
+        final RundeckClient client = createClient(TEST_TOKEN_7, 11);
+        client.deleteKey("keys/test/example/file2.pub");
+
+        try {
+            client.getKey("keys/test/example/file2.pub");
+            Assert.fail("expected failure");
+        } catch (RundeckApiException.RundeckApiHttpStatusException e) {
+            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());
+        }
+
+    }
+
+    /**
+     * 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()
+        );
+    }
+    /**
+     * 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
+     */
+    @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/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
new file mode 100644
index 0000000..26beb33
--- /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).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/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/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/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/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/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/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/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/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..3429369
--- /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/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+
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/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"
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=/
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=
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"
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/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"
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/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"
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
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=
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/get_executions.yaml b/src/test/resources/betamax/tapes/get_executions.yaml
index 337968b..f87fb2d 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
@@ -36,8 +38,9 @@ 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
       Proxy-Connection: Keep-Alive
       User-Agent: RunDeck API Java Client 5
@@ -51,8 +54,9 @@ 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
       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
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"
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/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==
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/betamax/tapes/key_delete.yaml b/src/test/resources/betamax/tapes/key_delete.yaml
new file mode 100644
index 0000000..b52ed7f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/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/keys/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/keys/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/key_get_data_private.yaml b/src/test/resources/betamax/tapes/key_get_data_private.yaml
new file mode 100644
index 0000000..ef0f58f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_data_private.yaml
@@ -0,0 +1,23 @@
+!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/keys/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 |-
+      eyJwYXRoIjoia2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtIiwidHlwZSI6ImZpbGUiLCJuYW1lIjoiZmlsZTEucGVtIiwidXJsIjoiaHR0cDovL2RpZ25hbi5sb2NhbDo0NDQwL2FwaS8xMS9zdG9yYWdlL2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbSIsIm1ldGEiOnsiUnVuZGVjay1jb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi9vY3RldC1zdHJlYW0iLCJSdW5kZWNrLWNvbnRlbnQtc2l6ZSI6IjUiLCJSdW5kZWNrLWNvbnRlbnQtbWFzayI6ImNvbnRlbnQiLCJSdW5kZWNrLWtleS10eXBlIjoicHJpdmF0ZSJ9fQo=
+
diff --git a/src/test/resources/betamax/tapes/key_get_data_public.yaml b/src/test/resources/betamax/tapes/key_get_data_public.yaml
new file mode 100644
index 0000000..054599f
--- /dev/null
+++ b/src/test/resources/betamax/tapes/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/keys/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/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/key_get_public.yaml b/src/test/resources/betamax/tapes/key_get_public.yaml
new file mode 100644
index 0000000..32c2510
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_get_public.yaml
@@ -0,0 +1,23 @@
+!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/keys/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 |-
+      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/key_store_private.yaml b/src/test/resources/betamax/tapes/key_store_private.yaml
new file mode 100644
index 0000000..1a5d0b0
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_store_private.yaml
@@ -0,0 +1,26 @@
+!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/keys/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 |-
+      PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUxLnBlbScgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTEucGVtJyBuYW1lPSdmaWxlMS5wZW0nPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9vY3RldC1zdHJlYW08L1J1bmRlY2stY29udGVudC10eXBlPjxSdW5kZWNrLWNvbnRlbnQtc2l6ZT41PC9SdW5kZWNrLWNvbnRlbnQtc2l6ZT48UnVuZGVjay1jb250ZW50LW1hc2s+Y29udGVudDwvUnVuZGVjay1jb250ZW50LW1hc2s+PFJ1bmRlY2sta2V5LXR5cGU+cHJpdmF0ZTwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4N
+
diff --git a/src/test/resources/betamax/tapes/key_store_public.yaml b/src/test/resources/betamax/tapes/key_store_public.yaml
new file mode 100644
index 0000000..fdd7638
--- /dev/null
+++ b/src/test/resources/betamax/tapes/key_store_public.yaml
@@ -0,0 +1,26 @@
+!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/keys/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 |-
+      PHJlc291cmNlIHBhdGg9J2tleXMvdGVzdC9leGFtcGxlL2ZpbGUyLnB1YicgdHlwZT0nZmlsZScgdXJsPSdodHRwOi8vZGlnbmFuLmxvY2FsOjQ0NDAvYXBpLzExL3N0b3JhZ2Uva2V5cy90ZXN0L2V4YW1wbGUvZmlsZTIucHViJyBuYW1lPSdmaWxlMi5wdWInPjxyZXNvdXJjZS1tZXRhPjxSdW5kZWNrLWNvbnRlbnQtdHlwZT5hcHBsaWNhdGlvbi9wZ3Ata2V5czwvUnVuZGVjay1jb250ZW50LXR5cGU+PFJ1bmRlY2stY29udGVudC1zaXplPjU8L1J1bmRlY2stY29udGVudC1zaXplPjxSdW5kZWNrLWtleS10eXBlPnB1YmxpYzwvUnVuZGVjay1rZXktdHlwZT48L3Jlc291cmNlLW1ldGE+PC9yZXNvdXJjZT4K
+
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+
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=
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"
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"
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
+            
+            
+                
+                
+                
+            
+            
+                
+                
+                
+            
+        
+    
+
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
+                    
+                
+            
+        
+    
+
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
+        
+            
+            
+            
+        
+    
+
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
+    
+    
+        
+        
+        
+        
+        
+        
+        
+        
+        
+        
+    
+
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 0000000..e5a1dab
Binary files /dev/null and b/src/test/resources/org/rundeck/api/test-archive.zip differ