diff --git a/src/main/java/org/rundeck/api/ApiCall.java b/src/main/java/org/rundeck/api/ApiCall.java index 7b71e46..c0a7625 100644 --- a/src/main/java/org/rundeck/api/ApiCall.java +++ b/src/main/java/org/rundeck/api/ApiCall.java @@ -18,6 +18,7 @@ package org.rundeck.api; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.ProxySelector; import java.security.KeyManagementException; import java.security.KeyStoreException; @@ -235,11 +236,21 @@ class ApiCall { HttpPost httpPost = new HttpPost(client.getUrl() + RundeckClient.API_ENDPOINT + apiPath); // POST a multi-part request, with all attachments - MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); - for (Entry attachment : apiPath.getAttachments().entrySet()) { - entity.addPart(attachment.getKey(), new InputStreamBody(attachment.getValue(), attachment.getKey())); + if(apiPath.getAttachments().size()>0){ + MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); + for (Entry attachment : apiPath.getAttachments().entrySet()) { + entity.addPart(attachment.getKey(), new InputStreamBody(attachment.getValue(), attachment.getKey())); + } + httpPost.setEntity(entity); + }else if(apiPath.getForm().size()>0){ + try { + httpPost.setEntity(new UrlEncodedFormEntity(apiPath.getForm(), HTTP.UTF_8)); + } catch (UnsupportedEncodingException e) { + throw new RundeckApiException("Unsupported encoding: " + e.getMessage(), e); + } + }else { + throw new IllegalArgumentException("No Form or Multipart entity for POST content-body"); } - httpPost.setEntity(entity); return execute(httpPost, parser); } diff --git a/src/main/java/org/rundeck/api/ApiPathBuilder.java b/src/main/java/org/rundeck/api/ApiPathBuilder.java index 39afd74..baa4c88 100644 --- a/src/main/java/org/rundeck/api/ApiPathBuilder.java +++ b/src/main/java/org/rundeck/api/ApiPathBuilder.java @@ -16,11 +16,17 @@ package org.rundeck.api; import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Properties; import org.apache.commons.lang.StringUtils; +import org.apache.http.NameValuePair; +import org.apache.http.entity.mime.FormBodyPart; +import org.apache.http.message.BasicNameValuePair; import org.rundeck.api.util.ParametersUtil; /** @@ -35,6 +41,7 @@ class ApiPathBuilder { /** When POSTing, we can add attachments */ private final Map attachments; + private final List form = new ArrayList(); /** Marker for using the right separator between parameters ("?" or "&") */ private boolean firstParamDone = false; @@ -76,6 +83,61 @@ class ApiPathBuilder { return this; } + /** + * Append the given parameter (key and value). This will only append the parameter if it is not blank (null, empty + * or whitespace), and make sure to add the right separator ("?" or "&") before. The key and value will be separated + * by the "=" character. Also, the value will be url-encoded. + * + * @param key of the parameter. Must not be null or empty + * @param value of the parameter. May be null/empty/blank. Will be url-encoded. + * @return this, for method chaining + */ + public ApiPathBuilder param(final String key, final Collection values) { + for(final String value: values){ + if (StringUtils.isNotBlank(value)) { + appendSeparator(); + append(key); + append("="); + append(ParametersUtil.urlEncode(value)); + } + } + return this; + } + + /** + * Append multiple values for the given Form field. This will be appended if it is not blank (null, empty + * or whitespace). The form field values will only be used for a "post" request + * + * @param key of the field name. Must not be null or empty + * @param values of the field. May be null/empty/blank. Will be url-encoded. + * @return this, for method chaining + */ + public ApiPathBuilder field(final String key, final Collection values) { + if (null!=values) { + for(final String value: values){ + if (StringUtils.isNotBlank(value)) { + form.add(new BasicNameValuePair(key, value)); + } + } + } + return this; + } + + /** + * Append a single value for the given Form field. This will be appended if it is not blank (null, empty + * or whitespace). The form field values will only be used for a "post" request + * + * @param key of the field name. Must not be null or empty + * @param value of the field. May be null/empty/blank. Will be url-encoded. + * @return this, for method chaining + */ + public ApiPathBuilder field(final String key, final String value) { + if (StringUtils.isNotBlank(value)) { + form.add(new BasicNameValuePair(key, value)); + } + return this; + } + /** * Append the given parameter (key and value). This will only append the parameter if it is not null, and make sure * to add the right separator ("?" or "&") before. The key and value will be separated by the "=" character. Also, @@ -216,4 +278,10 @@ class ApiPathBuilder { } } + /** + * Form fields for POST request + */ + public List getForm() { + return form; + } } diff --git a/src/main/java/org/rundeck/api/RundeckClient.java b/src/main/java/org/rundeck/api/RundeckClient.java index 2529942..92557f2 100644 --- a/src/main/java/org/rundeck/api/RundeckClient.java +++ b/src/main/java/org/rundeck/api/RundeckClient.java @@ -2065,7 +2065,7 @@ public class RundeckClient implements Serializable { */ public RundeckHistory getHistory(String project) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return getHistory(project, null, null, null, null, null, null, null, null); + return getHistory(project, null, null,(String) null, (String) null, null, null, null, null); } /** @@ -2083,7 +2083,7 @@ public class RundeckClient implements Serializable { */ public RundeckHistory getHistory(String project, Long max, Long offset) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return getHistory(project, null, null, null, null, null, null, max, offset); + return getHistory(project, null, null, (String)null, (String)null, null, null, max, offset); } /** @@ -2181,7 +2181,7 @@ public class RundeckClient implements Serializable { */ public RundeckHistory getHistory(String project, Date begin, Date end) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return getHistory(project, null, null, null, null, begin, end, null, null); + return getHistory(project, null, null, (String)null, (String)null, begin, end, null, null); } /** @@ -2201,7 +2201,7 @@ public class RundeckClient implements Serializable { */ public RundeckHistory getHistory(String project, Date begin, Date end, Long max, Long offset) throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { - return getHistory(project, null, null, null, null, begin, end, max, offset); + return getHistory(project, null, null, (String)null, (String) null, begin, end, max, offset); } /** @@ -2229,17 +2229,65 @@ public class RundeckClient implements Serializable { RundeckApiTokenException, IllegalArgumentException { AssertUtil.notBlank(project, "project is mandatory to get the history !"); return new ApiCall(this).get(new ApiPathBuilder("/history").param("project", project) - .param("jobIdFilter", jobId) - .param("reportIdFilter", reportId) - .param("userFilter", user) - .param("recentFilter", recent) - .param("begin", begin) - .param("end", end) - .param("max", max) - .param("offset", offset), + .param("jobIdFilter", jobId) + .param("reportIdFilter", reportId) + .param("userFilter", user) + .param("recentFilter", recent) + .param("begin", begin) + .param("end", end) + .param("max", max) + .param("offset", offset), new HistoryParser("result/events")); } + /** + * Get the (events) history for the given project + * + * @param project name of the project - mandatory + * @param includeJobNames list of job names ("group/name") to include results for + * @param excludeJobNames list of job names ("group/name") to exclude results for + * @param user include only events created by the given user - optional + * @param recent include only events matching the given period of time. Format : "XY", where X is an + * integer, and Y is one of : "h" (hour), "d" (day), "w" (week), "m" (month), "y" (year). + * Example : "2w" (= last 2 weeks), "5d" (= last 5 days), etc. Optional. + * @param begin date for the earlier events to retrieve - optional + * @param end date for the latest events to retrieve - optional + * @param max number of results to return - optional (default to 20) + * @param offset the 0-indexed offset for the first result to return - optional (default to O) + * + * @return a {@link RundeckHistory} 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 project is blank (null, empty or whitespace) + */ + public RundeckHistory getHistory(String project, + String user, + String recent, + List includeJobNames, + List excludeJobNames, + Date begin, + Date end, + Long max, + Long offset) + throws RundeckApiException, RundeckApiLoginException, RundeckApiTokenException, IllegalArgumentException { + + AssertUtil.notBlank(project, "project is mandatory to get the history !"); + final ApiPathBuilder builder = new ApiPathBuilder("/history").param("project", project) + .field("jobListFilter", includeJobNames) + .field("excludeJobListFilter", excludeJobNames) + .param("userFilter", user) + .param("recentFilter", recent) + .param("begin", begin) + .param("end", end) + .param("max", max) + .param("offset", offset); + + + return new ApiCall(this).post(builder, new HistoryParser("result/events")); + } + /* * Nodes */ diff --git a/src/test/java/org/rundeck/api/RundeckClientTest.java b/src/test/java/org/rundeck/api/RundeckClientTest.java index 19081ba..2d87304 100644 --- a/src/test/java/org/rundeck/api/RundeckClientTest.java +++ b/src/test/java/org/rundeck/api/RundeckClientTest.java @@ -15,11 +15,18 @@ */ package org.rundeck.api; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; + +import betamax.MatchRule; +import betamax.TapeMode; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.rundeck.api.domain.RundeckEvent; +import org.rundeck.api.domain.RundeckHistory; import org.rundeck.api.domain.RundeckProject; import betamax.Betamax; import betamax.Recorder; @@ -44,6 +51,55 @@ public class RundeckClientTest { Assert.assertEquals("test", projects.get(0).getName()); Assert.assertNull(projects.get(0).getDescription()); } + @Test + @Betamax(tape = "get_history") + public void getHistory() throws Exception { + final RundeckHistory test = client.getHistory("test"); + Assert.assertEquals(3, test.getCount()); + Assert.assertEquals(20, test.getMax()); + Assert.assertEquals(0, test.getOffset()); + Assert.assertEquals(5, test.getTotal()); + final List events = test.getEvents(); + Assert.assertEquals(3, events.size()); + } + + @Test + @Betamax(tape = "get_history_joblist", + match = {MatchRule.uri, MatchRule.method, MatchRule.path, MatchRule.query /*, MatchRule.body */}) + public void getHistoryJoblist() throws Exception { + final List jobNames = Arrays.asList("malk/blah", "malk/blah2"); + final RundeckHistory test = client.getHistory("demo", null, null, jobNames, null, null, null, null, null); + Assert.assertEquals(2, test.getCount()); + Assert.assertEquals(20, test.getMax()); + Assert.assertEquals(0, test.getOffset()); + Assert.assertEquals(2, test.getTotal()); + final List events = test.getEvents(); + Assert.assertEquals(2, events.size()); + final List names = new ArrayList(); + for (final RundeckEvent event : events) { + names.add(event.getTitle()); + } + Assert.assertEquals(Arrays.asList("malk/blah2", "malk/blah"), names); + } + + @Test + @Betamax(tape = "get_history_excludeJoblist", + match = {MatchRule.uri, MatchRule.method, MatchRule.path, MatchRule.query /*, MatchRule.body */}) + public void getHistoryExcludeJoblist() throws Exception { + final List jobNames = Arrays.asList("malk/blah", "malk/blah2"); + final RundeckHistory test = client.getHistory("demo", null, null, null, jobNames, null, null, null, null); + Assert.assertEquals(2, test.getCount()); + Assert.assertEquals(20, test.getMax()); + Assert.assertEquals(0, test.getOffset()); + Assert.assertEquals(2, test.getTotal()); + final List events = test.getEvents(); + Assert.assertEquals(2, events.size()); + final List names = new ArrayList(); + for (final RundeckEvent event : events) { + names.add(event.getTitle()); + } + Assert.assertEquals(Arrays.asList("fliff", "malk/blah3"), names); + } @Before public void setUp() throws Exception { diff --git a/src/test/resources/betamax.properties b/src/test/resources/betamax.properties index e212574..18bb949 100644 --- a/src/test/resources/betamax.properties +++ b/src/test/resources/betamax.properties @@ -1,4 +1,4 @@ betamax.tapeRoot=src/test/resources/betamax/tapes betamax.proxyPort=1337 betamax.proxyTimeout=5000 -betamax.defaultMode=READ_ONLY \ No newline at end of file +#betamax.defaultMode=READ_ONLY \ No newline at end of file diff --git a/src/test/resources/betamax/tapes/get_history.yaml b/src/test/resources/betamax/tapes/get_history.yaml new file mode 100644 index 0000000..029c4fa --- /dev/null +++ b/src/test/resources/betamax/tapes/get_history.yaml @@ -0,0 +1,20 @@ +!tape +name: get_projects +interactions: +- recorded: 2011-09-18T16:04:45.973Z + request: + method: GET + uri: http://rundeck.local:4440/api/5/history?project=test + headers: + Host: rundeck.local:4440 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 5 + X-RunDeck-Auth-Token: PVnN5K3OPc5vduS3uVuVnEsD57pDC5pd + response: + status: 200 + headers: + Content-Type: text/xml; charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(6.1.21) + Set-Cookie: JSESSIONID=mxrbh6byhvxt;Path=/ + body: malk/blah3succeededecho aljdsfadmindemo2012-08-23T23:21:37Z2012-08-23T23:21:37Zmalk/blah2succeededecho aljdsfadmindemo2012-08-23T23:21:31Z2012-08-23T23:21:31Zmalk/blahsucceededecho aljdsfadmindemo2012-08-23T23:21:23Z2012-08-23T23:21:25Z diff --git a/src/test/resources/betamax/tapes/get_history_excludeJoblist.yaml b/src/test/resources/betamax/tapes/get_history_excludeJoblist.yaml new file mode 100644 index 0000000..73be222 --- /dev/null +++ b/src/test/resources/betamax/tapes/get_history_excludeJoblist.yaml @@ -0,0 +1,23 @@ +!tape +name: get_history_excludeJoblist +interactions: +- recorded: 2012-08-24T21:55:44.759Z + request: + method: POST + uri: http://rundeck.local:4440/api/5/history?project=demo + headers: + Content-Length: '66' + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + Host: rundeck.localtest:8080 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 5 + X-RunDeck-Auth-Token: 960412PR40dRREU5d87S2Ce2OeddD5c1 + body: 'excludeJobListFilter=malk/blah&excludeJobListFilter=malk/blah2' + response: + status: 200 + headers: + Content-Type: text/xml; charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(6.1.21) + Set-Cookie: JSESSIONID=hgu6yb4dpooy;Path=/rundeck + body: fliffsucceededecho there i was not even counting itadmindemo2012-08-24T00:00:49Z2012-08-24T00:00:51Zmalk/blah3succeededecho aljdsfadmindemo2012-08-23T23:21:37Z2012-08-23T23:21:37Z diff --git a/src/test/resources/betamax/tapes/get_history_joblist.yaml b/src/test/resources/betamax/tapes/get_history_joblist.yaml new file mode 100644 index 0000000..3129bc4 --- /dev/null +++ b/src/test/resources/betamax/tapes/get_history_joblist.yaml @@ -0,0 +1,23 @@ +!tape +name: get_history_joblist +interactions: +- recorded: 2012-08-24T21:37:50.531Z + request: + method: POST + uri: http://rundeck.local:4440/api/5/history?project=demo + headers: + Content-Length: '52' + Content-Type: application/x-www-form-urlencoded; charset=UTF-8 + Host: rundeck.localtest:8080 + Proxy-Connection: Keep-Alive + User-Agent: RunDeck API Java Client 5 + X-RunDeck-Auth-Token: 960412PR40dRREU5d87S2Ce2OeddD5c1 + body: 'jobListFilter=malk/blah&jobListFilter=malk/blah2' + response: + status: 200 + headers: + Content-Type: text/xml; charset=utf-8 + Expires: Thu, 01 Jan 1970 00:00:00 GMT + Server: Jetty(6.1.21) + Set-Cookie: JSESSIONID=118zw5docyvss;Path=/rundeck + body: malk/blah2succeededecho aljdsfadmindemo2012-08-23T23:21:31Z2012-08-23T23:21:31Zmalk/blahsucceededecho aljdsfadmindemo2012-08-23T23:21:23Z2012-08-23T23:21:25Z