From 1594773a7956ccdf2e929b02d9aca9f2a7ce942b Mon Sep 17 00:00:00 2001 From: Vincent Behar Date: Thu, 7 Jul 2011 17:05:50 +0200 Subject: [PATCH] add support for exporting jobs (to file) --- pom.xml | 5 ++ src/main/java/org/rundeck/api/ApiCall.java | 50 +++++++++-- .../java/org/rundeck/api/RundeckClient.java | 84 +++++++++++++++++++ .../org/rundeck/api/parser/ParserHelper.java | 25 ------ 4 files changed, 131 insertions(+), 33 deletions(-) diff --git a/pom.xml b/pom.xml index 69b74fd..31f082c 100644 --- a/pom.xml +++ b/pom.xml @@ -397,6 +397,11 @@ commons-lang 2.6 + + commons-io + commons-io + 2.0.1 + dom4j diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 7acb0b7..2281804 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -15,7 +15,9 @@ */ package org.rundeck.api; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -117,6 +119,25 @@ class ApiCall { return execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath), 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 + * @throws RundeckApiLoginException if the login fails + */ + public InputStream get(ApiPathBuilder apiPath) throws RundeckApiException, RundeckApiLoginException { + ByteArrayInputStream response = execute(new HttpGet(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath)); + + // try to load the document, to throw an exception in case of error + ParserHelper.loadDocument(response); + response.reset(); + + return response; + } + /** * 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. @@ -144,6 +165,23 @@ class ApiCall { */ private T execute(HttpRequestBase request, XmlNodeParser parser) throws RundeckApiException, RundeckApiLoginException { + // execute the request + InputStream response = execute(request); + + // read and parse the response + Document xmlDocument = ParserHelper.loadDocument(response); + return parser.parseXmlNode(xmlDocument); + } + + /** + * 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 + */ + private ByteArrayInputStream execute(HttpRequestBase request) throws RundeckApiException, RundeckApiLoginException { HttpClient httpClient = instantiateHttpClient(); try { login(httpClient); @@ -176,6 +214,7 @@ class ApiCall { } } + // 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) { throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for " + request.getURI()); @@ -185,17 +224,12 @@ class ApiCall { + response.getStatusLine()); } - // read and parse the response - Document xmlDocument = ParserHelper.loadDocument(response); - T result = parser.parseXmlNode(xmlDocument); - - // release the connection + // return a new inputStream, so that we can close all network resources try { - EntityUtils.consume(response.getEntity()); + return new ByteArrayInputStream(EntityUtils.toByteArray(response.getEntity())); } catch (IOException e) { - throw new RundeckApiException("Failed to consume entity (release connection)", e); + throw new RundeckApiException("Failed to consume entity and convert the inputStream", e); } - return result; } finally { httpClient.getConnectionManager().shutdown(); } diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 02b8072..e1eb52a 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -15,11 +15,16 @@ */ package org.rundeck.api; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.Properties; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.rundeck.api.RundeckApiException.RundeckApiLoginException; import org.rundeck.api.domain.RundeckAbort; @@ -195,6 +200,85 @@ public class RundeckClient implements Serializable { new ListParser(new JobParser(), "result/jobs/job")); } + /** + * Export the definitions of all jobs that belongs to the given project, as an XML file + * + * @param filename path of the file where the content should be saved + * @param project 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 failed + * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) + * @throws IOException if we failed to write to the file + * @see #exportJobsToFile(String, String, String, String, String...) + * @see #exportJobs(String) + */ + public void exportJobsToFile(String filename, String project) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException, IOException { + exportJobsToFile(filename, project, null, null, new String[0]); + } + + /** + * Export the definitions of the jobs that belongs to the given project, and matches the given criteria (jobFilter, + * groupPath and jobIds), as an XML file + * + * @param filename path of the file where the content should be saved + * @param project name of the project - mandatory + * @param jobFilter a filter for the job Name - optional + * @param groupPath a group or partial group path to include all jobs within that group path - optional + * @param jobIds a list of Job IDs to include - optional + * @throws RundeckApiException in case of error when calling the API (non-existent project with this name) + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) + * @throws IOException if we failed to write to the file + * @see #exportJobsToFile(String, String) + * @see #exportJobs(String, String, String, String...) + */ + public void exportJobsToFile(String filename, String project, String jobFilter, String groupPath, String... jobIds) + throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException, IOException { + InputStream inputStream = exportJobs(project, jobFilter, groupPath, jobIds); + FileUtils.writeByteArrayToFile(new File(filename), IOUtils.toByteArray(inputStream)); + } + + /** + * Export the definitions of all jobs that belongs to the given project + * + * @param project name of the project - mandatory + * @return an {@link InputStream} instance, not linked to any network resources - 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 failed + * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) + * @see #exportJobs(String, String, String, String...) + * @see #exportJobsToFile(String, String) + */ + public InputStream exportJobs(String project) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException { + return exportJobs(project, null, null, new String[0]); + } + + /** + * Export the definitions of the jobs that belongs to the given project, and matches the given criteria (jobFilter, + * groupPath and jobIds) + * + * @param project name of the project - mandatory + * @param jobFilter a filter for the job Name - optional + * @param groupPath a group or partial group path to include all jobs within that group path - optional + * @param jobIds a list of Job IDs to include - optional + * @return an {@link InputStream} instance, not linked to any network resources - 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 failed + * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) + * @see #exportJobs(String) + * @see #exportJobsToFile(String, String, String, String, String...) + */ + public InputStream exportJobs(String project, String jobFilter, String groupPath, String... jobIds) + throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException { + AssertUtil.notBlank(project, "project is mandatory to export all jobs !"); + return new ApiCall(this).get(new ApiPathBuilder("/jobs/export").param("project", project) + .param("jobFilter", jobFilter) + .param("groupPath", groupPath) + .param("idlist", StringUtils.join(jobIds, ","))); + } + /** * Find a job, identified by its project, group and name. Note that the groupPath is optional, as a job does not * need to belong to a group (either pass null, or an empty string). diff --git a/src/main/java/org/rundeck/api/parser/ParserHelper.java b/src/main/java/org/rundeck/api/parser/ParserHelper.java index 6cfbbc7..139d945 100644 --- a/src/main/java/org/rundeck/api/parser/ParserHelper.java +++ b/src/main/java/org/rundeck/api/parser/ParserHelper.java @@ -15,9 +15,7 @@ */ package org.rundeck.api.parser; -import java.io.IOException; import java.io.InputStream; -import org.apache.http.HttpResponse; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Node; @@ -31,35 +29,12 @@ import org.rundeck.api.RundeckApiException; */ public class ParserHelper { - /** - * Load an XML {@link Document} from the given RunDeck {@link HttpResponse}. - * - * @param httpResponse from an API call to RunDeck - * @return an XML {@link Document} - * @throws RundeckApiException if we failed to read the response, or if the response is an error - * @see #loadDocument(InputStream) - */ - public static Document loadDocument(HttpResponse httpResponse) throws RundeckApiException { - InputStream inputStream = null; - - try { - inputStream = httpResponse.getEntity().getContent(); - } catch (IllegalStateException e) { - throw new RundeckApiException("Failed to read RunDeck reponse", e); - } catch (IOException e) { - throw new RundeckApiException("Failed to read RunDeck reponse", e); - } - - return loadDocument(inputStream); - } - /** * Load an XML {@link Document} from the given {@link InputStream} * * @param inputStream from an API call to RunDeck * @return an XML {@link Document} * @throws RundeckApiException if we failed to read the response, or if the response is an error - * @see #loadDocument(HttpResponse) */ public static Document loadDocument(InputStream inputStream) throws RundeckApiException { SAXReader reader = new SAXReader();