From d294673b45f3a75c75cc482499711156fb01ffbb Mon Sep 17 00:00:00 2001 From: Vincent Behar Date: Fri, 1 Jul 2011 17:13:26 +0200 Subject: [PATCH] initial commit --- README | 8 + pom.xml | 81 ++++++ src/main/java/org/rundeck/api/ApiCall.java | 220 +++++++++++++++++ .../org/rundeck/api/RundeckApiException.java | 36 +++ .../java/org/rundeck/api/RundeckClient.java | 231 ++++++++++++++++++ .../rundeck/api/domain/RundeckExecution.java | 196 +++++++++++++++ .../org/rundeck/api/domain/RundeckJob.java | 132 ++++++++++ .../rundeck/api/parser/ExecutionParser.java | 58 +++++ .../rundeck/api/parser/ExecutionsParser.java | 40 +++ .../org/rundeck/api/parser/JobParser.java | 57 +++++ .../org/rundeck/api/parser/JobsParser.java | 40 +++ .../org/rundeck/api/parser/NodeParser.java | 20 ++ .../org/rundeck/api/parser/ParserHelper.java | 72 ++++++ .../java/org/rundeck/api/util/ArgsUtil.java | 72 ++++++ .../java/org/rundeck/api/util/AssertUtil.java | 38 +++ .../api/parser/ExecutionParserTest.java | 67 +++++ .../api/parser/ExecutionsParserTest.java | 65 +++++ .../org/rundeck/api/parser/JobParserTest.java | 30 +++ .../rundeck/api/parser/JobsParserTest.java | 40 +++ .../rundeck/api/parser/ParserHelperTest.java | 50 ++++ .../org/rundeck/api/util/ArgsUtilTest.java | 30 +++ .../org/rundeck/api/parser/empty.xml | 1 + .../org/rundeck/api/parser/error.xml | 1 + .../rundeck/api/parser/execution-running.xml | 1 + .../api/parser/execution-succeeded.xml | 1 + .../org/rundeck/api/parser/executions.xml | 1 + .../resources/org/rundeck/api/parser/job.xml | 62 +++++ .../resources/org/rundeck/api/parser/jobs.xml | 1 + .../org/rundeck/api/parser/success.xml | 1 + 29 files changed, 1652 insertions(+) create mode 100644 README create mode 100644 pom.xml create mode 100644 src/main/java/org/rundeck/api/ApiCall.java create mode 100644 src/main/java/org/rundeck/api/RundeckApiException.java create mode 100644 src/main/java/org/rundeck/api/RundeckClient.java create mode 100644 src/main/java/org/rundeck/api/domain/RundeckExecution.java create mode 100644 src/main/java/org/rundeck/api/domain/RundeckJob.java create mode 100644 src/main/java/org/rundeck/api/parser/ExecutionParser.java create mode 100644 src/main/java/org/rundeck/api/parser/ExecutionsParser.java create mode 100644 src/main/java/org/rundeck/api/parser/JobParser.java create mode 100644 src/main/java/org/rundeck/api/parser/JobsParser.java create mode 100644 src/main/java/org/rundeck/api/parser/NodeParser.java create mode 100644 src/main/java/org/rundeck/api/parser/ParserHelper.java create mode 100644 src/main/java/org/rundeck/api/util/ArgsUtil.java create mode 100644 src/main/java/org/rundeck/api/util/AssertUtil.java create mode 100644 src/test/java/org/rundeck/api/parser/ExecutionParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/ExecutionsParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/JobParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/JobsParserTest.java create mode 100644 src/test/java/org/rundeck/api/parser/ParserHelperTest.java create mode 100644 src/test/java/org/rundeck/api/util/ArgsUtilTest.java create mode 100644 src/test/resources/org/rundeck/api/parser/empty.xml create mode 100644 src/test/resources/org/rundeck/api/parser/error.xml create mode 100644 src/test/resources/org/rundeck/api/parser/execution-running.xml create mode 100644 src/test/resources/org/rundeck/api/parser/execution-succeeded.xml create mode 100644 src/test/resources/org/rundeck/api/parser/executions.xml create mode 100644 src/test/resources/org/rundeck/api/parser/job.xml create mode 100644 src/test/resources/org/rundeck/api/parser/jobs.xml create mode 100644 src/test/resources/org/rundeck/api/parser/success.xml diff --git a/README b/README new file mode 100644 index 0000000..2ff0000 --- /dev/null +++ b/README @@ -0,0 +1,8 @@ + +Java client for the RunDeck REST API + +RunDeck Home : http://rundeck.org/ + +LICENSE : The Apache Software License, Version 2.0 +See http://www.apache.org/licenses/LICENSE-2.0.txt + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1445c5c --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + org.sonatype.oss + oss-parent + 7 + + + org.rundeck + rundeck-api-java-client + 1.0-SNAPSHOT + jar + + RunDeck API - Java Client + Java client for the RunDeck REST API + https://github.com/vbehar/rundeck-api-java-client + + https://github.com/vbehar/rundeck-api-java-client + scm:git:git://github.com/vbehar/rundeck-api-java-client.git + scm:git:git@github.com:vbehar/rundeck-api-java-client.git + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + vbehar + Vincent Behar + + + + + UTF-8 + + + + + + org.apache.httpcomponents + httpclient + 4.1.1 + + + + + commons-lang + commons-lang + 2.6 + + + + + dom4j + dom4j + 1.6.1 + + + jaxen + jaxen + 1.1.1 + + + + + junit + junit + 4.8.1 + test + + + + + diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java new file mode 100644 index 0000000..ccefb97 --- /dev/null +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -0,0 +1,220 @@ +package org.rundeck.api; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.ParseException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +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.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.protocol.HTTP; +import org.apache.http.util.EntityUtils; +import org.dom4j.Document; +import org.rundeck.api.RundeckApiException.RundeckApiLoginException; +import org.rundeck.api.parser.NodeParser; +import org.rundeck.api.parser.ParserHelper; + +/** + * Class responsible for making the HTTP API calls + * + * @author Vincent Behar + */ +class ApiCall { + + private final RundeckClient client; + + /** + * Build a new instance, linked to the given RunDeck client + * + * @param client holding the RunDeck url and the credentials + */ + public ApiCall(RundeckClient client) { + super(); + this.client = client; + } + + /** + * Try to "ping" the RunDeck instance to see if it is alive + * + * @throws RundeckApiException if the ping fails + */ + public void ping() throws RundeckApiException { + HttpClient httpClient = instantiateHttpClient(); + try { + HttpResponse response = httpClient.execute(new HttpGet(client.getUrl())); + if (response.getStatusLine().getStatusCode() / 100 != 2) { + throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' when pinging " + + client.getUrl()); + } + } catch (IOException e) { + throw new RundeckApiException("Failed to ping RunDeck instance at " + client.getUrl(), e); + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * Test the credentials (login/password) on the RunDeck instance + * + * @throws RundeckApiLoginException if the login fails + */ + public void testCredentials() throws RundeckApiLoginException { + HttpClient httpClient = instantiateHttpClient(); + try { + login(httpClient); + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * 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 + * @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 + */ + public T get(String apiPath, NodeParser parser) throws RundeckApiException, RundeckApiLoginException { + String apiUrl = client.getUrl() + RundeckClient.API_ENDPOINT + apiPath; + + HttpClient httpClient = instantiateHttpClient(); + try { + login(httpClient); + + // execute the HTTP request + HttpResponse response = null; + try { + response = httpClient.execute(new HttpGet(apiUrl)); + } catch (IOException e) { + throw new RundeckApiException("Failed to execute an HTTP GET on url : " + apiUrl, e); + } + if (response.getStatusLine().getStatusCode() / 100 != 2) { + throw new RundeckApiException("Invalid HTTP response '" + response.getStatusLine() + "' for " + apiUrl); + } + if (response.getEntity() == null) { + throw new RundeckApiException("Empty RunDeck response ! HTTP status line is : " + + response.getStatusLine()); + } + + // read and parse the response + Document xmlDocument = ParserHelper.loadDocument(response); + T result = parser.parseNode(xmlDocument); + + // release the connection + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiException("Failed to consume entity (release connection)", e); + } + return result; + } finally { + httpClient.getConnectionManager().shutdown(); + } + } + + /** + * 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). + * + * @param httpClient pre-instantiated + * @throws RundeckApiLoginException if the login failed + */ + private void login(HttpClient httpClient) throws RundeckApiLoginException { + String location = client.getUrl() + "/j_security_check"; + + while (true) { + HttpPost postLogin = new HttpPost(location); + List params = new ArrayList(); + params.add(new BasicNameValuePair("j_username", client.getLogin())); + params.add(new BasicNameValuePair("j_password", client.getPassword())); + params.add(new BasicNameValuePair("action", "login")); + + HttpResponse response = null; + try { + postLogin.setEntity(new UrlEncodedFormEntity(params, HTTP.UTF_8)); + response = httpClient.execute(postLogin); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to post login form on " + location, e); + } + + if (response.getStatusLine().getStatusCode() / 100 == 3) { + // HTTP client refuses to handle redirects (code 3xx) for POST, so we have to do it manually... + location = response.getFirstHeader("Location").getValue(); + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + continue; + } + if (response.getStatusLine().getStatusCode() / 100 != 2) { + throw new RundeckApiLoginException("Invalid HTTP response '" + response.getStatusLine() + "' for " + + location); + } + try { + String content = EntityUtils.toString(response.getEntity(), HTTP.UTF_8); + if (StringUtils.contains(content, "j_security_check")) { + throw new RundeckApiLoginException("Login failed for user " + client.getLogin()); + } + try { + EntityUtils.consume(response.getEntity()); + } catch (IOException e) { + throw new RundeckApiLoginException("Failed to consume entity (release connection)", e); + } + } catch (IOException io) { + throw new RundeckApiLoginException("Failed to read RunDeck result", io); + } catch (ParseException p) { + throw new RundeckApiLoginException("Failed to parse RunDeck response", p); + } + break; + } + } + + /** + * Instantiate a new {@link HttpClient} instance, configured to accept all SSL certificates + * + * @return an {@link HttpClient} instance - won't be null + */ + private HttpClient instantiateHttpClient() { + SSLSocketFactory socketFactory = null; + try { + socketFactory = new SSLSocketFactory(new TrustStrategy() { + + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { + return true; + } + }, SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + } catch (KeyManagementException e) { + throw new RuntimeException(e); + } catch (UnrecoverableKeyException e) { + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + + HttpClient httpClient = new DefaultHttpClient(); + httpClient.getConnectionManager().getSchemeRegistry().register(new Scheme("https", 443, socketFactory)); + return httpClient; + } + +} diff --git a/src/main/java/org/rundeck/api/RundeckApiException.java b/src/main/java/org/rundeck/api/RundeckApiException.java new file mode 100644 index 0000000..a7810bb --- /dev/null +++ b/src/main/java/org/rundeck/api/RundeckApiException.java @@ -0,0 +1,36 @@ +package org.rundeck.api; + +/** + * A generic (unchecked) exception when using the RunDeck API + * + * @author Vincent Behar + */ +public class RundeckApiException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + public RundeckApiException(String message) { + super(message); + } + + public RundeckApiException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Specific login-related error + */ + public static class RundeckApiLoginException extends RundeckApiException { + + private static final long serialVersionUID = 1L; + + public RundeckApiLoginException(String message) { + super(message); + } + + public RundeckApiLoginException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java new file mode 100644 index 0000000..e1a9abd --- /dev/null +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -0,0 +1,231 @@ +package org.rundeck.api; + +import java.io.Serializable; +import java.util.List; +import java.util.Properties; +import org.apache.commons.lang.StringUtils; +import org.rundeck.api.RundeckApiException.RundeckApiLoginException; +import org.rundeck.api.domain.RundeckExecution; +import org.rundeck.api.domain.RundeckJob; +import org.rundeck.api.parser.ExecutionParser; +import org.rundeck.api.parser.ExecutionsParser; +import org.rundeck.api.parser.JobParser; +import org.rundeck.api.parser.JobsParser; +import org.rundeck.api.util.ArgsUtil; +import org.rundeck.api.util.AssertUtil; + +/** + * Main entry point to talk to a RunDeck instance + * + * @author Vincent Behar + */ +public class RundeckClient implements Serializable { + + private static final long serialVersionUID = 1L; + + public static final transient int API_VERSION = 1; + + public static final transient String API_ENDPOINT = "/api/" + API_VERSION; + + private final String url; + + private final String login; + + private final String password; + + /** + * Instantiate a new {@link RundeckClient} for the RunDeck instance at the given url + * + * @param url of the RunDeck instance ("http://localhost:4440", "http://rundeck.your-compagny.com/", etc) + * @param login + * @param password + */ + public RundeckClient(String url, String login, String password) { + super(); + this.url = url; + this.login = login; + this.password = password; + } + + /** + * Try to "ping" the RunDeck instance to see if it is alive + * + * @throws RundeckApiException if the ping fails + */ + public void ping() throws RundeckApiException { + new ApiCall(this).ping(); + } + + /** + * Test your credentials (login/password) on the RunDeck instance + * + * @throws RundeckApiLoginException if the login fails + */ + public void testCredentials() throws RundeckApiLoginException { + new ApiCall(this).testCredentials(); + } + + /** + * List all jobs that belongs to the given project + * + * @param project name of the project - mandatory + * @return a {@link List} of {@link RundeckJob} : might be empty, but won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) + * @see #getJobs(String, String, String, String...) + */ + public List getJobs(String project) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException { + return getJobs(project, null, null, new String[0]); + } + + /** + * List 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 a {@link List} of {@link RundeckJob} : might be empty, but won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the project is blank (null, empty or whitespace) + * @see #getJobs(String) + */ + public List getJobs(String project, String jobFilter, String groupPath, String... jobIds) + throws RundeckApiException, RundeckApiLoginException, IllegalArgumentException { + AssertUtil.notBlank(project, "project is mandatory to get all jobs !"); + StringBuilder apiPath = new StringBuilder("/jobs"); + apiPath.append("?project=").append(project); + if (StringUtils.isNotBlank(jobFilter)) { + apiPath.append("&jobFilter=").append(jobFilter); + } + if (StringUtils.isNotBlank(groupPath)) { + apiPath.append("&groupPath=").append(groupPath); + } + if (jobIds != null && jobIds.length > 0) { + apiPath.append("&idlist=").append(StringUtils.join(jobIds, ",")); + } + return new ApiCall(this).get(apiPath.toString(), new JobsParser("result/jobs/job")); + } + + /** + * Get the definition of a single job, identified by the given ID + * + * @param jobId identifier of the job - mandatory + * @return a {@link RundeckJob} instance + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckJob getJob(String jobId) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException { + AssertUtil.notBlank(jobId, "jobId is mandatory to get the details of a job !"); + return new ApiCall(this).get("/job/" + jobId, new JobParser("joblist/job")); + } + + /** + * Get the executions of the given job + * + * @param jobId identifier of the job - mandatory + * @return a {@link List} of {@link RundeckExecution} : might be empty, but won't be null + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public List getJobExecutions(String jobId) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException { + AssertUtil.notBlank(jobId, "jobId is mandatory to get the executions of a job !"); + return new ApiCall(this).get("/job/" + jobId + "/executions", + new ExecutionsParser("result/executions/execution")); + } + + /** + * Get a single execution, identified by the given ID + * + * @param executionId identifier of the execution - mandatory + * @return a {@link RundeckExecution} instance + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the executionId is null + */ + public RundeckExecution getExecution(Long executionId) throws RundeckApiException, RundeckApiLoginException, + IllegalArgumentException { + AssertUtil.notNull(executionId, "executionId is mandatory to get the details of an execution !"); + return new ApiCall(this).get("/execution/" + executionId, new ExecutionParser("result/executions/execution")); + } + + /** + * 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 + * @return a {@link RundeckExecution} instance representing the newly created (and running) execution + * @throws RundeckApiException in case of error when calling the API + * @throws RundeckApiLoginException if the login failed + * @throws IllegalArgumentException if the jobId is blank (null, empty or whitespace) + */ + public RundeckExecution triggerJob(String jobId, Properties options) throws RundeckApiException, + RundeckApiLoginException, IllegalArgumentException { + AssertUtil.notBlank(jobId, "jobId is mandatory to trigger a job !"); + String apiPath = "/job/" + jobId + "/run?argString=" + ArgsUtil.generateUrlEncodedArgString(options); + return new ApiCall(this).get(apiPath, new ExecutionParser("result/executions/execution")); + } + + public String getUrl() { + return url; + } + + public String getLogin() { + return login; + } + + public String getPassword() { + return password; + } + + @Override + public String toString() { + return "RundeckClient [url=" + url + ", login=" + login + ", password=" + password + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((login == null) ? 0 : login.hashCode()); + result = prime * result + ((password == null) ? 0 : password.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RundeckClient other = (RundeckClient) obj; + if (login == null) { + if (other.login != null) + return false; + } else if (!login.equals(other.login)) + return false; + if (password == null) { + if (other.password != null) + return false; + } else if (!password.equals(other.password)) + return false; + if (url == null) { + if (other.url != null) + return false; + } else if (!url.equals(other.url)) + return false; + return true; + } + +} diff --git a/src/main/java/org/rundeck/api/domain/RundeckExecution.java b/src/main/java/org/rundeck/api/domain/RundeckExecution.java new file mode 100644 index 0000000..a5405b4 --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckExecution.java @@ -0,0 +1,196 @@ +package org.rundeck.api.domain; + +import java.io.Serializable; +import java.util.Date; + +/** + * Represents a RunDeck execution, usually triggered by an API call. An execution could be a {@link RundeckJob} + * execution or an "ad-hoc" execution. + * + * @author Vincent Behar + */ +public class RundeckExecution implements Serializable { + + private static final long serialVersionUID = 1L; + + private Long id; + + private String url; + + private ExecutionStatus status; + + /** Optional - only if it is a job execution */ + private RundeckJob job; + + private String startedBy; + + private Date startedAt; + + /** only if the execution has ended */ + private Date endedAt; + + /** only if the execution was aborted */ + private String abortedBy; + + private String description; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public ExecutionStatus getStatus() { + return status; + } + + public void setStatus(ExecutionStatus status) { + this.status = status; + } + + public RundeckJob getJob() { + return job; + } + + public void setJob(RundeckJob job) { + this.job = job; + } + + public String getStartedBy() { + return startedBy; + } + + public void setStartedBy(String startedBy) { + this.startedBy = startedBy; + } + + public Date getStartedAt() { + return startedAt; + } + + public void setStartedAt(Date startedAt) { + this.startedAt = startedAt; + } + + public Date getEndedAt() { + return endedAt; + } + + public void setEndedAt(Date endedAt) { + this.endedAt = endedAt; + } + + public String getAbortedBy() { + return abortedBy; + } + + public void setAbortedBy(String abortedBy) { + this.abortedBy = abortedBy; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return "RundeckExecution [abortedBy=" + abortedBy + ", description=" + description + ", endedAt=" + endedAt + + ", id=" + id + ", job=" + job + ", startedAt=" + startedAt + ", startedBy=" + startedBy + ", status=" + + status + ", url=" + url + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((abortedBy == null) ? 0 : abortedBy.hashCode()); + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((endedAt == null) ? 0 : endedAt.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((job == null) ? 0 : job.hashCode()); + result = prime * result + ((startedAt == null) ? 0 : startedAt.hashCode()); + result = prime * result + ((startedBy == null) ? 0 : startedBy.hashCode()); + result = prime * result + ((status == null) ? 0 : status.hashCode()); + result = prime * result + ((url == null) ? 0 : url.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RundeckExecution other = (RundeckExecution) obj; + if (abortedBy == null) { + if (other.abortedBy != null) + return false; + } else if (!abortedBy.equals(other.abortedBy)) + return false; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + if (endedAt == null) { + if (other.endedAt != null) + return false; + } else if (!endedAt.equals(other.endedAt)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (job == null) { + if (other.job != null) + return false; + } else if (!job.equals(other.job)) + return false; + if (startedAt == null) { + if (other.startedAt != null) + return false; + } else if (!startedAt.equals(other.startedAt)) + return false; + if (startedBy == null) { + if (other.startedBy != null) + return false; + } else if (!startedBy.equals(other.startedBy)) + return false; + if (status == null) { + if (other.status != null) + return false; + } else if (!status.equals(other.status)) + return false; + if (url == null) { + if (other.url != null) + return false; + } else if (!url.equals(other.url)) + return false; + return true; + } + + /** + * The status of an execution + */ + public static enum ExecutionStatus { + RUNNING, SUCCEEDED, FAILED, ABORTED; + } + +} diff --git a/src/main/java/org/rundeck/api/domain/RundeckJob.java b/src/main/java/org/rundeck/api/domain/RundeckJob.java new file mode 100644 index 0000000..b1cac7d --- /dev/null +++ b/src/main/java/org/rundeck/api/domain/RundeckJob.java @@ -0,0 +1,132 @@ +package org.rundeck.api.domain; + +import java.io.Serializable; +import org.apache.commons.lang.StringUtils; + +/** + * Represents a RunDeck job + * + * @author Vincent Behar + */ +public class RundeckJob implements Serializable { + + private static final long serialVersionUID = 1L; + + private String id; + + private String name; + + private String group; + + private String project; + + private String description; + + /** + * @return the fullname : group + name (exact format is : "group/name") + */ + public String getFullName() { + StringBuilder fullName = new StringBuilder(); + if (StringUtils.isNotBlank(group)) { + fullName.append(group).append("/"); + } + fullName.append(name); + return fullName.toString(); + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getGroup() { + return group; + } + + public void setGroup(String group) { + this.group = group; + } + + public String getProject() { + return project; + } + + public void setProject(String project) { + this.project = project; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public String toString() { + return "RundeckJob [id=" + id + ", name=" + name + ", group=" + group + ", project=" + project + + ", description=" + description + "]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((description == null) ? 0 : description.hashCode()); + result = prime * result + ((group == null) ? 0 : group.hashCode()); + result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((name == null) ? 0 : name.hashCode()); + result = prime * result + ((project == null) ? 0 : project.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + RundeckJob other = (RundeckJob) obj; + if (description == null) { + if (other.description != null) + return false; + } else if (!description.equals(other.description)) + return false; + if (group == null) { + if (other.group != null) + return false; + } else if (!group.equals(other.group)) + return false; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + if (project == null) { + if (other.project != null) + return false; + } else if (!project.equals(other.project)) + return false; + return true; + } + +} diff --git a/src/main/java/org/rundeck/api/parser/ExecutionParser.java b/src/main/java/org/rundeck/api/parser/ExecutionParser.java new file mode 100644 index 0000000..43ee79f --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/ExecutionParser.java @@ -0,0 +1,58 @@ +package org.rundeck.api.parser; + +import java.util.Date; +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; + +/** + * Parser for a single {@link RundeckExecution} + * + * @author Vincent Behar + */ +public class ExecutionParser implements NodeParser { + + private String xpath; + + public ExecutionParser() { + super(); + } + + /** + * @param xpath of the execution element if it is not the root node + */ + public ExecutionParser(String xpath) { + super(); + this.xpath = xpath; + } + + @Override + public RundeckExecution parseNode(Node node) { + Node execNode = xpath != null ? node.selectSingleNode(xpath) : node; + + RundeckExecution execution = new RundeckExecution(); + + execution.setId(Long.valueOf(execNode.valueOf("@id"))); + execution.setUrl(StringUtils.trimToNull(execNode.valueOf("@href"))); + execution.setStatus(ExecutionStatus.valueOf(StringUtils.upperCase(execNode.valueOf("@status")))); + execution.setDescription(StringUtils.trimToNull(execNode.valueOf("description"))); + execution.setStartedBy(StringUtils.trimToNull(execNode.valueOf("user"))); + execution.setStartedAt(new Date(Long.valueOf(execNode.valueOf("date-started/@unixtime")))); + execution.setAbortedBy(StringUtils.trimToNull(execNode.valueOf("abortedby"))); + String endedAt = StringUtils.trimToNull(execNode.valueOf("date-ended/@unixtime")); + if (endedAt != null) { + execution.setEndedAt(new Date(Long.valueOf(endedAt))); + } + + Node jobNode = execNode.selectSingleNode("job"); + if (jobNode != null) { + RundeckJob job = new JobParser().parseNode(jobNode); + execution.setJob(job); + } + + return execution; + } + +} diff --git a/src/main/java/org/rundeck/api/parser/ExecutionsParser.java b/src/main/java/org/rundeck/api/parser/ExecutionsParser.java new file mode 100644 index 0000000..2b542f9 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/ExecutionsParser.java @@ -0,0 +1,40 @@ +package org.rundeck.api.parser; + +import java.util.ArrayList; +import java.util.List; +import org.dom4j.Node; +import org.rundeck.api.domain.RundeckExecution; + +/** + * Parser for a {@link List} of {@link RundeckExecution} + * + * @author Vincent Behar + */ +public class ExecutionsParser implements NodeParser> { + + private final String xpath; + + /** + * @param xpath of the executions elements + */ + public ExecutionsParser(String xpath) { + super(); + this.xpath = xpath; + } + + @Override + public List parseNode(Node node) { + List executions = new ArrayList(); + + @SuppressWarnings("unchecked") + List execNodes = node.selectNodes(xpath); + + for (Node execNode : execNodes) { + RundeckExecution execution = new ExecutionParser().parseNode(execNode); + executions.add(execution); + } + + return executions; + } + +} diff --git a/src/main/java/org/rundeck/api/parser/JobParser.java b/src/main/java/org/rundeck/api/parser/JobParser.java new file mode 100644 index 0000000..b840bdc --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/JobParser.java @@ -0,0 +1,57 @@ +package org.rundeck.api.parser; + +import org.apache.commons.lang.StringUtils; +import org.dom4j.Node; +import org.rundeck.api.domain.RundeckJob; + +/** + * Parser for a single {@link RundeckJob} + * + * @author Vincent Behar + */ +public class JobParser implements NodeParser { + + private String xpath; + + public JobParser() { + super(); + } + + /** + * @param xpath of the job element if it is not the root node + */ + public JobParser(String xpath) { + super(); + this.xpath = xpath; + } + + @Override + public RundeckJob parseNode(Node node) { + Node jobNode = xpath != null ? node.selectSingleNode(xpath) : node; + + RundeckJob job = new RundeckJob(); + + job.setName(StringUtils.trimToNull(jobNode.valueOf("name"))); + job.setDescription(StringUtils.trimToNull(jobNode.valueOf("description"))); + job.setGroup(StringUtils.trimToNull(jobNode.valueOf("group"))); + + // ID is either an attribute or an child element... + String jobId = null; + jobId = jobNode.valueOf("id"); + if (StringUtils.isBlank(jobId)) { + jobId = jobNode.valueOf("@id"); + } + job.setId(jobId); + + // project is either a nested element of context, or just a child element + Node contextNode = jobNode.selectSingleNode("context"); + if (contextNode != null) { + job.setProject(StringUtils.trimToNull(contextNode.valueOf("project"))); + } else { + job.setProject(StringUtils.trimToNull(jobNode.valueOf("project"))); + } + + return job; + } + +} diff --git a/src/main/java/org/rundeck/api/parser/JobsParser.java b/src/main/java/org/rundeck/api/parser/JobsParser.java new file mode 100644 index 0000000..d48da4a --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/JobsParser.java @@ -0,0 +1,40 @@ +package org.rundeck.api.parser; + +import java.util.ArrayList; +import java.util.List; +import org.dom4j.Node; +import org.rundeck.api.domain.RundeckJob; + +/** + * Parser for a {@link List} of {@link RundeckJob} + * + * @author Vincent Behar + */ +public class JobsParser implements NodeParser> { + + private final String xpath; + + /** + * @param xpath of the jobs elements + */ + public JobsParser(String xpath) { + super(); + this.xpath = xpath; + } + + @Override + public List parseNode(Node node) { + List jobs = new ArrayList(); + + @SuppressWarnings("unchecked") + List jobNodes = node.selectNodes(xpath); + + for (Node jobNode : jobNodes) { + RundeckJob job = new JobParser().parseNode(jobNode); + jobs.add(job); + } + + return jobs; + } + +} diff --git a/src/main/java/org/rundeck/api/parser/NodeParser.java b/src/main/java/org/rundeck/api/parser/NodeParser.java new file mode 100644 index 0000000..58a80e2 --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/NodeParser.java @@ -0,0 +1,20 @@ +package org.rundeck.api.parser; + +import org.dom4j.Node; + +/** + * Interface to be implemented for parsers that handle XML {@link Node}s + * + * @author Vincent Behar + */ +public interface NodeParser { + + /** + * Parse the given XML {@link Node} + * + * @param node + * @return any object holding the converted value + */ + T parseNode(Node node); + +} diff --git a/src/main/java/org/rundeck/api/parser/ParserHelper.java b/src/main/java/org/rundeck/api/parser/ParserHelper.java new file mode 100644 index 0000000..fd5aa0a --- /dev/null +++ b/src/main/java/org/rundeck/api/parser/ParserHelper.java @@ -0,0 +1,72 @@ +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; +import org.dom4j.io.SAXReader; +import org.rundeck.api.RundeckApiException; + +/** + * Helper for parsing RunDeck responses + * + * @author Vincent Behar + */ +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(); + reader.setEncoding("UTF-8"); + + Document document; + try { + document = reader.read(inputStream); + } catch (DocumentException e) { + throw new RundeckApiException("Failed to read RunDeck reponse", e); + } + document.setXMLEncoding("UTF-8"); + + Node result = document.selectSingleNode("result"); + if (result != null) { + Boolean failure = Boolean.valueOf(result.valueOf("@error")); + if (failure) { + throw new RundeckApiException(result.valueOf("error/message")); + } + } + + return document; + } + +} diff --git a/src/main/java/org/rundeck/api/util/ArgsUtil.java b/src/main/java/org/rundeck/api/util/ArgsUtil.java new file mode 100644 index 0000000..2948dde --- /dev/null +++ b/src/main/java/org/rundeck/api/util/ArgsUtil.java @@ -0,0 +1,72 @@ +package org.rundeck.api.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Properties; +import java.util.Map.Entry; +import org.apache.commons.lang.StringUtils; + +/** + * Utility class for RunDeck arguments + * + * @author Vincent Behar + */ +public class ArgsUtil { + + /** + * Generates and url-encode a RunDeck "argString" representing the given options. Format of the argString is + * "-key1 value1 -key2 'value 2 with spaces'" + * + * @param options to be converted + * @return an url-encoded string. null if options is null, empty if there are no valid options. + * @see #generateArgString(Properties) + */ + public static String generateUrlEncodedArgString(Properties options) { + String argString = generateArgString(options); + if (StringUtils.isBlank(argString)) { + return argString; + } + + try { + return URLEncoder.encode(argString, "UTF8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + /** + * Generates a RunDeck "argString" representing the given options. Format of the argString is + * "-key1 value1 -key2 'value 2 with spaces'" + * + * @param options to be converted + * @return a string. null if options is null, empty if there are no valid options. + * @see #generateUrlEncodedArgString(Properties) + */ + public static String generateArgString(Properties options) { + if (options == null) { + return null; + } + + StringBuilder argString = new StringBuilder(); + for (Entry option : options.entrySet()) { + String key = String.valueOf(option.getKey()); + String value = String.valueOf(option.getValue()); + + if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) { + if (argString.length() > 0) { + argString.append(" "); + } + argString.append("-").append(key); + argString.append(" "); + if (value.indexOf(" ") >= 0 + && !(0 == value.indexOf("'") && (value.length() - 1) == value.lastIndexOf("'"))) { + argString.append("'").append(value).append("'"); + } else { + argString.append(value); + } + } + } + return argString.toString(); + } + +} diff --git a/src/main/java/org/rundeck/api/util/AssertUtil.java b/src/main/java/org/rundeck/api/util/AssertUtil.java new file mode 100644 index 0000000..de5a23e --- /dev/null +++ b/src/main/java/org/rundeck/api/util/AssertUtil.java @@ -0,0 +1,38 @@ +package org.rundeck.api.util; + +import org.apache.commons.lang.StringUtils; + +/** + * Utility class for assertions + * + * @author Vincent Behar + */ +public class AssertUtil { + + /** + * Test if the given {@link Object} is null + * + * @param object + * @param errorMessage to be used if the object is null + * @throws IllegalArgumentException if the given object is null + */ + public static void notNull(Object object, String errorMessage) throws IllegalArgumentException { + if (object == null) { + throw new IllegalArgumentException(errorMessage); + } + } + + /** + * Test if the given {@link String} is blank (null, empty or only whitespace) + * + * @param input string + * @param errorMessage to be used if the string is blank + * @throws IllegalArgumentException if the given string is blank + */ + public static void notBlank(String input, String errorMessage) throws IllegalArgumentException { + if (StringUtils.isBlank(input)) { + throw new IllegalArgumentException(errorMessage); + } + } + +} diff --git a/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java b/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java new file mode 100644 index 0000000..d81fe66 --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/ExecutionParserTest.java @@ -0,0 +1,67 @@ +package org.rundeck.api.parser; + +import java.io.InputStream; +import java.util.Date; +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; + +/** + * Test the {@link ExecutionParser} + * + * @author Vincent Behar + */ +public class ExecutionParserTest { + + @Test + public void parseRunningNode() throws Exception { + InputStream input = getClass().getResourceAsStream("execution-running.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckExecution execution = new ExecutionParser("result/executions/execution").parseNode(document); + RundeckJob job = execution.getJob(); + + Assert.assertEquals(new Long(1), execution.getId()); + Assert.assertEquals("http://localhost:4440/execution/follow/1", execution.getUrl()); + Assert.assertEquals(ExecutionStatus.RUNNING, execution.getStatus()); + Assert.assertEquals("admin", execution.getStartedBy()); + Assert.assertEquals(new Date(1302183830082L), execution.getStartedAt()); + Assert.assertEquals(null, execution.getEndedAt()); + Assert.assertEquals(null, execution.getAbortedBy()); + Assert.assertEquals("ls ${option.dir}", execution.getDescription()); + + Assert.assertEquals("1", job.getId()); + Assert.assertEquals("ls", job.getName()); + Assert.assertEquals("system", job.getGroup()); + Assert.assertEquals("test", job.getProject()); + Assert.assertEquals("list files", job.getDescription()); + } + + @Test + public void parseSucceededNode() throws Exception { + InputStream input = getClass().getResourceAsStream("execution-succeeded.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckExecution execution = new ExecutionParser("result/executions/execution").parseNode(document); + RundeckJob job = execution.getJob(); + + Assert.assertEquals(new Long(1), execution.getId()); + Assert.assertEquals("http://localhost:4440/execution/follow/1", execution.getUrl()); + Assert.assertEquals(ExecutionStatus.SUCCEEDED, execution.getStatus()); + Assert.assertEquals("admin", execution.getStartedBy()); + Assert.assertEquals(new Date(1308322895104L), execution.getStartedAt()); + Assert.assertEquals(new Date(1308322959420L), execution.getEndedAt()); + Assert.assertEquals(null, execution.getAbortedBy()); + Assert.assertEquals("ls ${option.dir}", execution.getDescription()); + + Assert.assertEquals("1", job.getId()); + Assert.assertEquals("ls", job.getName()); + Assert.assertEquals("system", job.getGroup()); + Assert.assertEquals("test", job.getProject()); + Assert.assertEquals("list files", job.getDescription()); + } + +} diff --git a/src/test/java/org/rundeck/api/parser/ExecutionsParserTest.java b/src/test/java/org/rundeck/api/parser/ExecutionsParserTest.java new file mode 100644 index 0000000..d5f536d --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/ExecutionsParserTest.java @@ -0,0 +1,65 @@ +package org.rundeck.api.parser; + +import java.io.InputStream; +import java.util.Date; +import java.util.List; +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; + +/** + * Test the {@link ExecutionsParser} + * + * @author Vincent Behar + */ +public class ExecutionsParserTest { + + @Test + public void parseNode() throws Exception { + InputStream input = getClass().getResourceAsStream("executions.xml"); + Document document = ParserHelper.loadDocument(input); + + List executions = new ExecutionsParser("result/executions/execution").parseNode(document); + Assert.assertEquals(2, executions.size()); + + RundeckExecution exec1 = executions.get(0); + Assert.assertEquals(new Long(1), exec1.getId()); + Assert.assertEquals("http://localhost:4440/execution/follow/1", exec1.getUrl()); + Assert.assertEquals(ExecutionStatus.SUCCEEDED, exec1.getStatus()); + Assert.assertEquals("admin", exec1.getStartedBy()); + Assert.assertEquals(new Date(1308322895104L), exec1.getStartedAt()); + Assert.assertEquals(new Date(1308322959420L), exec1.getEndedAt()); + Assert.assertEquals(null, exec1.getAbortedBy()); + Assert.assertEquals("ls ${option.dir}", exec1.getDescription()); + + RundeckExecution exec2 = executions.get(1); + Assert.assertEquals(new Long(2), exec2.getId()); + Assert.assertEquals("http://localhost:4440/execution/follow/2", exec2.getUrl()); + Assert.assertEquals(ExecutionStatus.SUCCEEDED, exec2.getStatus()); + Assert.assertEquals("admin", exec2.getStartedBy()); + Assert.assertEquals(new Date(1309524165388L), exec2.getStartedAt()); + Assert.assertEquals(new Date(1309524174635L), exec2.getEndedAt()); + Assert.assertEquals(null, exec2.getAbortedBy()); + Assert.assertEquals("ls ${option.dir}", exec2.getDescription()); + + RundeckJob job1 = exec1.getJob(); + Assert.assertEquals("1", job1.getId()); + Assert.assertEquals("ls", job1.getName()); + Assert.assertEquals("system", job1.getGroup()); + Assert.assertEquals("test", job1.getProject()); + Assert.assertEquals("list files", job1.getDescription()); + + RundeckJob job2 = exec2.getJob(); + Assert.assertEquals("1", job2.getId()); + Assert.assertEquals("ls", job2.getName()); + Assert.assertEquals("system", job2.getGroup()); + Assert.assertEquals("test", job2.getProject()); + Assert.assertEquals("list files", job2.getDescription()); + + Assert.assertEquals(job1, job2); + } + +} diff --git a/src/test/java/org/rundeck/api/parser/JobParserTest.java b/src/test/java/org/rundeck/api/parser/JobParserTest.java new file mode 100644 index 0000000..a68e68f --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/JobParserTest.java @@ -0,0 +1,30 @@ +package org.rundeck.api.parser; + +import java.io.InputStream; +import org.dom4j.Document; +import org.junit.Assert; +import org.junit.Test; +import org.rundeck.api.domain.RundeckJob; + +/** + * Test the {@link JobParser} + * + * @author Vincent Behar + */ +public class JobParserTest { + + @Test + public void parseNode() throws Exception { + InputStream input = getClass().getResourceAsStream("job.xml"); + Document document = ParserHelper.loadDocument(input); + + RundeckJob job = new JobParser("joblist/job").parseNode(document); + + Assert.assertEquals("1", job.getId()); + Assert.assertEquals("job-name", job.getName()); + Assert.assertEquals("job description", job.getDescription()); + Assert.assertEquals("group-name", job.getGroup()); + Assert.assertEquals("project-name", job.getProject()); + } + +} diff --git a/src/test/java/org/rundeck/api/parser/JobsParserTest.java b/src/test/java/org/rundeck/api/parser/JobsParserTest.java new file mode 100644 index 0000000..35503fe --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/JobsParserTest.java @@ -0,0 +1,40 @@ +package org.rundeck.api.parser; + +import java.io.InputStream; +import java.util.List; +import org.dom4j.Document; +import org.junit.Assert; +import org.junit.Test; +import org.rundeck.api.domain.RundeckJob; + +/** + * Test the {@link JobsParser} + * + * @author Vincent Behar + */ +public class JobsParserTest { + + @Test + public void parseNode() throws Exception { + InputStream input = getClass().getResourceAsStream("jobs.xml"); + Document document = ParserHelper.loadDocument(input); + + List jobs = new JobsParser("result/jobs/job").parseNode(document); + Assert.assertEquals(2, jobs.size()); + + RundeckJob job1 = jobs.get(0); + Assert.assertEquals("1", job1.getId()); + Assert.assertEquals("ls", job1.getName()); + Assert.assertEquals("list files", job1.getDescription()); + Assert.assertEquals("system", job1.getGroup()); + Assert.assertEquals("test", job1.getProject()); + + RundeckJob job2 = jobs.get(1); + Assert.assertEquals("2", job2.getId()); + Assert.assertEquals("ps", job2.getName()); + Assert.assertEquals("list processes", job2.getDescription()); + Assert.assertEquals("system", job2.getGroup()); + Assert.assertEquals("test", job2.getProject()); + } + +} diff --git a/src/test/java/org/rundeck/api/parser/ParserHelperTest.java b/src/test/java/org/rundeck/api/parser/ParserHelperTest.java new file mode 100644 index 0000000..c745cbe --- /dev/null +++ b/src/test/java/org/rundeck/api/parser/ParserHelperTest.java @@ -0,0 +1,50 @@ +package org.rundeck.api.parser; + +import java.io.InputStream; +import org.dom4j.Document; +import org.junit.Assert; +import org.junit.Test; +import org.rundeck.api.RundeckApiException; + +/** + * Test the {@link ParserHelper} + * + * @author Vincent Behar + */ +public class ParserHelperTest { + + /** + * XML with an explicit "error" result should throw an exception + */ + @Test + public void loadErrorDocument() throws Exception { + InputStream input = getClass().getResourceAsStream("error.xml"); + try { + ParserHelper.loadDocument(input); + Assert.fail("should have thrown an exception !"); + } catch (RundeckApiException e) { + Assert.assertEquals("This is the error message", e.getMessage()); + } + } + + /** + * XML with an explicit "success" result should NOT throw an exception + */ + @Test + public void loadSuccessDocument() throws Exception { + InputStream input = getClass().getResourceAsStream("success.xml"); + Document document = ParserHelper.loadDocument(input); + Assert.assertNotNull(document); + } + + /** + * XML with no result should NOT throw an exception + */ + @Test + public void loadEmptyDocument() throws Exception { + InputStream input = getClass().getResourceAsStream("empty.xml"); + Document document = ParserHelper.loadDocument(input); + Assert.assertNotNull(document); + } + +} diff --git a/src/test/java/org/rundeck/api/util/ArgsUtilTest.java b/src/test/java/org/rundeck/api/util/ArgsUtilTest.java new file mode 100644 index 0000000..f89d788 --- /dev/null +++ b/src/test/java/org/rundeck/api/util/ArgsUtilTest.java @@ -0,0 +1,30 @@ +package org.rundeck.api.util; + +import java.util.Properties; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test the {@link ArgsUtil} + * + * @author Vincent Behar + */ +public class ArgsUtilTest { + + @Test + public void generateArgString() throws Exception { + Assert.assertNull(ArgsUtil.generateArgString(null)); + Assert.assertEquals("", ArgsUtil.generateArgString(new Properties())); + + Properties options = new Properties(); + options.put("key1", "value1"); + options.put("key2", "value 2 with spaces"); + String argString = ArgsUtil.generateArgString(options); + if (argString.startsWith("-key1")) { + Assert.assertEquals("-key1 value1 -key2 'value 2 with spaces'", argString); + } else { + Assert.assertEquals("-key2 'value 2 with spaces' -key1 value1", argString); + } + } + +} diff --git a/src/test/resources/org/rundeck/api/parser/empty.xml b/src/test/resources/org/rundeck/api/parser/empty.xml new file mode 100644 index 0000000..4455e1d --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/empty.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/error.xml b/src/test/resources/org/rundeck/api/parser/error.xml new file mode 100644 index 0000000..678397e --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/error.xml @@ -0,0 +1 @@ +This is the error message \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/execution-running.xml b/src/test/resources/org/rundeck/api/parser/execution-running.xml new file mode 100644 index 0000000..262da63 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/execution-running.xml @@ -0,0 +1 @@ +admin2011-04-07T13:43:50Zlssystemtestlist filesls ${option.dir} \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/execution-succeeded.xml b/src/test/resources/org/rundeck/api/parser/execution-succeeded.xml new file mode 100644 index 0000000..1deb6c9 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/execution-succeeded.xml @@ -0,0 +1 @@ +admin2011-06-17T15:01:35Z2011-06-17T15:02:39Zlssystemtestlist filesls ${option.dir} \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/executions.xml b/src/test/resources/org/rundeck/api/parser/executions.xml new file mode 100644 index 0000000..6570edc --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/executions.xml @@ -0,0 +1 @@ +admin2011-06-17T15:01:35Z2011-06-17T15:02:39Zlssystemtestlist filesls ${option.dir}admin2011-07-01T12:42:45Z2011-07-01T12:42:54Zlssystemtestlist filesls ${option.dir} \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/job.xml b/src/test/resources/org/rundeck/api/parser/job.xml new file mode 100644 index 0000000..bac291e --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/job.xml @@ -0,0 +1,62 @@ + + + 1 + + + INFO + + + echo ${option.opt1} ${option.opt2} ${option.opt3} + + + options + + + + job description + job-name + + project-name + + + + + + + + + + + + + + + + + 2 + true + true + + + + tag + name + + + group-name + + \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/jobs.xml b/src/test/resources/org/rundeck/api/parser/jobs.xml new file mode 100644 index 0000000..c861e24 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/jobs.xml @@ -0,0 +1 @@ +lssystemtestlist filespssystemtestlist processes \ No newline at end of file diff --git a/src/test/resources/org/rundeck/api/parser/success.xml b/src/test/resources/org/rundeck/api/parser/success.xml new file mode 100644 index 0000000..425dac3 --- /dev/null +++ b/src/test/resources/org/rundeck/api/parser/success.xml @@ -0,0 +1 @@ + \ No newline at end of file