diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 94243f29..4be5026d 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -3,6 +3,13 @@ ## Release 0.3.8 (NOT RELEASED YET) + * [Fixed Issue 301][issue-301] + + Decoupled JenkinsServer and JenkinsHttpClient by extracting JenkinsHttpClient + methods into public interface so that different implementations can be plugged + into JenkinsServer if required + + * [Fixed Issue 298][issue-298] Added Closeable support to JenkinsServer and JenkinsHttpClient so that diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java index 6f707824..87f5a0b0 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/JenkinsServer.java @@ -25,6 +25,7 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import com.offbytwo.jenkins.client.JenkinsHttpClient; +import com.offbytwo.jenkins.client.JenkinsHttpConnection; import com.offbytwo.jenkins.client.util.EncodingUtils; import com.offbytwo.jenkins.client.util.UrlUtils; import com.offbytwo.jenkins.helper.JenkinsVersion; @@ -51,7 +52,10 @@ public class JenkinsServer implements Closeable { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); - private final JenkinsHttpClient client; + /** + * The transport client instance to use. + */ + private final JenkinsHttpConnection client; /** * Create a new Jenkins server reference given only the server address @@ -80,7 +84,7 @@ public JenkinsServer(URI serverUri, String username, String passwordOrToken) { * * @param client Specialized client to use. */ - public JenkinsServer(JenkinsHttpClient client) { + public JenkinsServer(final JenkinsHttpConnection client) { this.client = client; } diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java index ce7428c8..31c64141 100755 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpClient.java @@ -3,7 +3,6 @@ * * Distributed under the MIT license: http://opensource.org/licenses/MIT */ - package com.offbytwo.jenkins.client; import com.fasterxml.jackson.databind.ObjectMapper; @@ -49,12 +48,10 @@ import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; import com.offbytwo.jenkins.client.util.ResponseUtils; import com.offbytwo.jenkins.client.util.UrlUtils; -import java.io.Closeable; import static org.apache.commons.lang.StringUtils.isNotBlank; -//import com.offbytwo.jenkins.client.util.HttpResponseContentExtractor; +public class JenkinsHttpClient implements JenkinsHttpConnection { -public class JenkinsHttpClient implements Closeable { private final Logger LOGGER = LoggerFactory.getLogger(getClass()); private URI uri; @@ -69,7 +66,7 @@ public class JenkinsHttpClient implements Closeable { private String jenkinsVersion; public final static String EMPTY_VERSION = "UNKNOWN"; - + /** * Create an unauthenticated Jenkins HTTP client * @@ -137,14 +134,9 @@ public JenkinsHttpClient(URI uri, HttpClientBuilder builder, String username, St } /** - * Perform a GET request and parse the response to the given class - * - * @param path path to request, can be relative or absolute - * @param cls class of the response - * @param type of the response - * @return an instance of the supplied class - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public T get(String path, Class cls) throws IOException { HttpGet getMethod = new HttpGet(UrlUtils.toJsonApiUri(uri, context, path)); @@ -160,13 +152,9 @@ public T get(String path, Class cls) throws IOException } /** - * Perform a GET request and parse the response and return a simple string - * of the content - * - * @param path path to request, can be relative or absolute - * @return the entity text - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public String get(String path) throws IOException { HttpGet getMethod = new HttpGet(UrlUtils.toJsonApiUri(uri, context, path)); HttpResponse response = client.execute(getMethod, localContext); @@ -184,14 +172,9 @@ public String get(String path) throws IOException { } /** - * Perform a GET request and parse the response to the given class, logging - * any IOException that is thrown rather than propagating it. - * - * @param path path to request, can be relative or absolute - * @param cls class of the response - * @param type of the response - * @return an instance of the supplied class + * {@inheritDoc} */ + @Override public T getQuietly(String path, Class cls) { T value; try { @@ -205,12 +188,9 @@ public T getQuietly(String path, Class cls) { } /** - * Perform a GET request and return the response as InputStream - * - * @param path path to request, can be relative or absolute - * @return the response stream - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public InputStream getFile(URI path) throws IOException { HttpGet getMethod = new HttpGet(path); HttpResponse response = client.execute(getMethod, localContext); @@ -219,22 +199,18 @@ public InputStream getFile(URI path) throws IOException { return new RequestReleasingInputStream(response.getEntity().getContent(), getMethod); } + /** + * {@inheritDoc} + */ + @Override public R post(String path, D data, Class cls) throws IOException { return post(path, data, cls, true); } /** - * Perform a POST request and parse the response to the given class - * - * @param path path to request, can be relative or absolute - * @param data data to post - * @param cls class of the response - * @param type of the response - * @param type of the data - * @param crumbFlag true / false. - * @return an instance of the supplied class - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public R post(String path, D data, Class cls, boolean crumbFlag) throws IOException { HttpPost request = new HttpPost(UrlUtils.toJsonApiUri(uri, context, path)); if (crumbFlag == true) { @@ -275,23 +251,9 @@ public R post(String path, D data, Class cls, boolea } /** - * Perform a POST request using form url encoding. - * - * This method was added for the purposes of creating folders, but may be - useful for other API calls as well. - - Unlike post and post_xml, the path is *not* modified by adding - "/toJsonApiUri/json". Additionally, the params in data are provided as both - request parameters including a json parameter, *and* in the - JSON-formatted StringEntity, because this is what the folder creation - call required. It is unclear if any other jenkins APIs operate in this - fashion. - * - * @param path path to request, can be relative or absolute - * @param data data to post - * @param crumbFlag true / false. - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public void post_form(String path, Map data, boolean crumbFlag) throws IOException { HttpPost request; if (data != null) { @@ -329,18 +291,10 @@ public void post_form(String path, Map data, boolean crumbFlag) } } - /** - * Perform a POST request using form url encoding and return HttpResponse object - * This method is not performing validation and can be used for more generic queries to jenkins. - * - * @param path - * path to request, can be relative or absolute - * @param data - * data to post - * @throws IOException, - * HttpResponseException + * {@inheritDoc} */ + @Override public HttpResponse post_form_with_result(String path, List data, boolean crumbFlag) throws IOException { HttpPost request; if (data != null) { @@ -363,18 +317,17 @@ public HttpResponse post_form_with_result(String path, List data, } /** - * Perform a POST request of XML (instead of using json mapper) and return a - * string rendering of the response entity. - * - * @param path path to request, can be relative or absolute - * @param xml_data data data to post - * @return A string containing the xml response (if present) - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public String post_xml(String path, String xml_data) throws IOException { return post_xml(path, xml_data, true); } + /** + * {@inheritDoc} + */ + @Override public String post_xml(String path, String xml_data, boolean crumbFlag) throws IOException { HttpPost request = new HttpPost(UrlUtils.toJsonApiUri(uri, context, path)); if (crumbFlag == true) { @@ -399,28 +352,17 @@ public String post_xml(String path, String xml_data, boolean crumbFlag) throws I } /** - * Post a text entity to the given URL using the default content type - * - * @param path The path. - * @param textData data. - * @param crumbFlag true/false. - * @return resulting response - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public String post_text(String path, String textData, boolean crumbFlag) throws IOException { return post_text(path, textData, ContentType.DEFAULT_TEXT, crumbFlag); } /** - * Post a text entity to the given URL with the given content type - * - * @param path The path. - * @param textData The data. - * @param contentType {@link ContentType} - * @param crumbFlag true or false. - * @return resulting response - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public String post_text(String path, String textData, ContentType contentType, boolean crumbFlag) throws IOException { HttpPost request = new HttpPost(UrlUtils.toJsonApiUri(uri, context, path)); @@ -446,67 +388,41 @@ public String post_text(String path, String textData, ContentType contentType, b } /** - * Perform POST request that takes no parameters and returns no response - * - * @param path path to request - * @throws IOException in case of an error. + * {@inheritDoc} */ + @Override public void post(String path) throws IOException { post(path, null, null, false); } + /** + * {@inheritDoc} + */ + @Override public void post(String path, boolean crumbFlag) throws IOException { post(path, null, null, crumbFlag); } - - - - - - - private T objectFromResponse(Class cls, HttpResponse response) throws IOException { - InputStream content = response.getEntity().getContent(); - byte[] bytes = ByteStreams.toByteArray(content); - T result = mapper.readValue(bytes, cls); - // TODO: original: - // T result = mapper.readValue(content, cls); - result.setClient(this); - return result; - } - - private ObjectMapper getDefaultMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES); - return mapper; - } - /** - * @return the version string. + * {@inheritDoc} */ + @Override public String getJenkinsVersion() { return this.jenkinsVersion; } /** - * Check to see if the jenkins version has been set - * to something different than the initialization value - * from the constructor. This means there has never been made - * a communication with the Jenkins server. - * @return true if jenkinsVersion has been set by communication, false otherwise. + * {@inheritDoc} */ + @Override public boolean isJenkinsVersionSet() { - return !EMPTY_VERSION.equals( this.jenkinsVersion ); + return !EMPTY_VERSION.equals(this.jenkinsVersion); } - - - - + /** - * Closes underlying resources. - * Any I/O errors whilst closing are logged with debug log level - * Closed instances should no longer be used - * Closing an already closed instance has no side effects + * Closes underlying resources. Any I/O errors whilst closing are logged + * with debug log level Closed instances should no longer be used Closing an + * already closed instance has no side effects */ @Override public void close() { @@ -514,17 +430,20 @@ public void close() { client.close(); } catch (final IOException ex) { LOGGER.debug("I/O exception closing client", ex); - } + } } - - - private void releaseConnection(HttpRequestBase httpRequestBase) { - httpRequestBase.releaseConnection(); - } - - protected static HttpClientBuilder addAuthentication(HttpClientBuilder builder, URI uri, String username, + /** + * Add authentication to supplied builder. + * @param builder the builder to configure + * @param uri the server URI + * @param username the username + * @param password the password + * @return the passed in builder + */ + protected static HttpClientBuilder addAuthentication(final HttpClientBuilder builder, + final URI uri, final String username, String password) { if (isNotBlank(username)) { CredentialsProvider provider = new BasicCredentialsProvider(); @@ -538,13 +457,46 @@ protected static HttpClientBuilder addAuthentication(HttpClientBuilder builder, return builder; } + + /** + * Get the local context. + * @return context + */ protected HttpContext getLocalContext() { return localContext; } - protected void setLocalContext(HttpContext localContext) { + + /** + * Set the local context. + * @param localContext the context + */ + protected void setLocalContext(final HttpContext localContext) { this.localContext = localContext; } + + + + private T objectFromResponse(Class cls, HttpResponse response) throws IOException { + InputStream content = response.getEntity().getContent(); + byte[] bytes = ByteStreams.toByteArray(content); + T result = mapper.readValue(bytes, cls); + // TODO: original: + // T result = mapper.readValue(content, cls); + result.setClient(this); + return result; + } + + private ObjectMapper getDefaultMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.disable(FAIL_ON_UNKNOWN_PROPERTIES); + return mapper; + } + + private void releaseConnection(HttpRequestBase httpRequestBase) { + httpRequestBase.releaseConnection(); + } + } diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpConnection.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpConnection.java new file mode 100644 index 00000000..5e1997fa --- /dev/null +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/client/JenkinsHttpConnection.java @@ -0,0 +1,214 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package com.offbytwo.jenkins.client; + +import com.offbytwo.jenkins.model.BaseModel; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.entity.ContentType; + +/** + * + * @author Dell Green + */ +public interface JenkinsHttpConnection extends Closeable { + + + + /** + * {@inheritDoc} + */ + @Override + public void close(); + + + /** + * Perform a GET request and parse the response to the given class + * + * @param path path to request, can be relative or absolute + * @param cls class of the response + * @param type of the response + * @return an instance of the supplied class + * @throws IOException in case of an error. + */ + T get(String path, Class cls) throws IOException; + + /** + * Perform a GET request and parse the response and return a simple string + * of the content + * + * @param path path to request, can be relative or absolute + * @return the entity text + * @throws IOException in case of an error. + */ + String get(String path) throws IOException; + + /** + * Perform a GET request and return the response as InputStream + * + * @param path path to request, can be relative or absolute + * @return the response stream + * @throws IOException in case of an error. + */ + InputStream getFile(URI path) throws IOException; + + /** + * Perform a GET request and parse the response to the given class, logging + * any IOException that is thrown rather than propagating it. + * + * @param path path to request, can be relative or absolute + * @param cls class of the response + * @param type of the response + * @return an instance of the supplied class + */ + T getQuietly(String path, Class cls); + + /** + * Check to see if the Jenkins version has been set to something different + * than the initialisation value from the constructor. This means there has + * never been made a communication with the Jenkins server. + * @return true if jenkinsVersion has been set by communication, false + * otherwise. + */ + boolean isJenkinsVersionSet(); + + + /** + * Perform a POST request and parse the response to the given class + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param cls class of the response + * @param type of the response + * @param type of the data + * @return an instance of the supplied class + * @throws IOException in case of an error. + */ + R post(String path, D data, Class cls) throws IOException; + + /** + * Perform a POST request and parse the response to the given class + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param cls class of the response + * @param type of the response + * @param type of the data + * @param crumbFlag true / false. + * @return an instance of the supplied class + * @throws IOException in case of an error. + */ + R post(String path, D data, Class cls, boolean crumbFlag) throws IOException; + + /** + * Perform POST request that takes no parameters and returns no response + * + * @param path path to request + * @throws IOException in case of an error. + */ + void post(String path) throws IOException; + + + /** + * Perform POST request that takes no parameters and returns no response + * + * @param path path to request + * @param crumbFlag true / false. + * @throws IOException in case of an error. + */ + void post(String path, boolean crumbFlag) throws IOException; + + /** + * Perform a POST request using form url encoding. + * + * This method was added for the purposes of creating folders, but may be + * useful for other API calls as well. Unlike post and post_xml, the path is + * *not* modified by adding "/toJsonApiUri/json". Additionally, the params + * in data are provided as both request parameters including a json + * parameter, *and* in the JSON-formatted StringEntity, because this is what + * the folder creation call required. It is unclear if any other jenkins + * APIs operate in this fashion. + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param crumbFlag true / false. + * @throws IOException in case of an error. + */ + void post_form(String path, Map data, boolean crumbFlag) throws IOException; + + /** + * Perform a POST request using form url encoding and return HttpResponse + * object This method is not performing validation and can be used for more + * generic queries to jenkins. + * + * @param path path to request, can be relative or absolute + * @param data data to post + * @param crumbFlag true / false. + * @return resulting response + * @throws IOException, HttpResponseException + */ + HttpResponse post_form_with_result(String path, List data, boolean crumbFlag) throws IOException; + + /** + * Post a text entity to the given URL using the default content type + * + * @param path The path. + * @param textData data. + * @param crumbFlag true/false. + * @return resulting response + * @throws IOException in case of an error. + */ + String post_text(String path, String textData, boolean crumbFlag) throws IOException; + + /** + * Post a text entity to the given URL with the given content type + * + * @param path The path. + * @param textData The data. + * @param contentType {@link ContentType} + * @param crumbFlag true or false. + * @return resulting response + * @throws IOException in case of an error. + */ + String post_text(String path, String textData, ContentType contentType, boolean crumbFlag) throws IOException; + + /** + * Perform a POST request of XML (instead of using json mapper) and return a + * string rendering of the response entity. + * + * @param path path to request, can be relative or absolute + * @param xml_data data data to post + * @return A string containing the xml response (if present) + * @throws IOException in case of an error. + */ + String post_xml(String path, String xml_data) throws IOException; + + /** + * Perform a POST request of XML (instead of using json mapper) and return a + * string rendering of the response entity. + * + * @param path path to request, can be relative or absolute + * @param xml_data data data to post + * @param crumbFlag true or false. + * @return A string containing the xml response (if present) + * @throws IOException in case of an error. + */ + String post_xml(String path, String xml_data, boolean crumbFlag) throws IOException; + + /** + * Get the Jenkins server version. + * + * @return the version string + */ + String getJenkinsVersion(); + +} diff --git a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/BaseModel.java b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/BaseModel.java index dd473ee8..90d7f793 100644 --- a/jenkins-client/src/main/java/com/offbytwo/jenkins/model/BaseModel.java +++ b/jenkins-client/src/main/java/com/offbytwo/jenkins/model/BaseModel.java @@ -6,28 +6,44 @@ package com.offbytwo.jenkins.model; -import com.offbytwo.jenkins.client.JenkinsHttpClient; +import com.offbytwo.jenkins.client.JenkinsHttpConnection; /** * The base model. - * */ public class BaseModel { + /** + * The class. + */ private String _class; + + /** + * Get the class. + * @return class + */ public String get_class() { return _class; } //TODO: We should make this private - protected JenkinsHttpClient client; + protected JenkinsHttpConnection client; - public JenkinsHttpClient getClient() { + + /** + * Get the HTTP client. + * @return client + */ + public JenkinsHttpConnection getClient() { return client; } - public void setClient(JenkinsHttpClient client) { + /** + * Set the HTTP client. + * @param client + */ + public void setClient(final JenkinsHttpConnection client) { this.client = client; } } diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java index e9853b30..9e9d50aa 100644 --- a/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/JenkinsServerTest.java @@ -27,6 +27,7 @@ import com.google.common.base.Optional; import com.offbytwo.jenkins.client.JenkinsHttpClient; +import com.offbytwo.jenkins.client.JenkinsHttpConnection; import com.offbytwo.jenkins.model.FolderJob; import com.offbytwo.jenkins.model.Job; import com.offbytwo.jenkins.model.JobWithDetails; @@ -41,7 +42,7 @@ public class JenkinsServerTest extends BaseUnitTest { - private JenkinsHttpClient client = mock(JenkinsHttpClient.class); + private JenkinsHttpConnection client = mock(JenkinsHttpClient.class); private JenkinsServer server = new JenkinsServer(client); private MainView mainView = new MainView(new Job("Hello", "http://localhost/job/Hello/")); diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/client/JenkinsHttpClientTest.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/client/JenkinsHttpClientTest.java index b4a121d6..80f3ba38 100644 --- a/jenkins-client/src/test/java/com/offbytwo/jenkins/client/JenkinsHttpClientTest.java +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/client/JenkinsHttpClientTest.java @@ -47,7 +47,7 @@ public void testGet_String() throws Exception { given(statusLine.getStatusCode()).willReturn(HttpStatus.SC_OK); given(response.getEntity()).willReturn(entity); given(entity.getContent()).willReturn(new ByteArrayInputStream("someJson".getBytes())); - final JenkinsHttpClient jclient = new JenkinsHttpClient(new URI(URI), client); + final JenkinsHttpConnection jclient = new JenkinsHttpClient(new URI(URI), client); final String s = jclient.get("job/someJob"); assertEquals("someJson", s); } @@ -56,7 +56,7 @@ public void testGet_String() throws Exception { @Test(expected=IllegalStateException.class) public void testClose() throws Exception { - final JenkinsHttpClient jclient = new JenkinsHttpClient(new URI(URI)); + final JenkinsHttpConnection jclient = new JenkinsHttpClient(new URI(URI)); jclient.close(); jclient.close(); //check multiple calls yield no errors jclient.get("job/someJob"); diff --git a/jenkins-client/src/test/java/com/offbytwo/jenkins/model/BuildWithDetailsTest.java b/jenkins-client/src/test/java/com/offbytwo/jenkins/model/BuildWithDetailsTest.java index af25f266..83ddd07b 100644 --- a/jenkins-client/src/test/java/com/offbytwo/jenkins/model/BuildWithDetailsTest.java +++ b/jenkins-client/src/test/java/com/offbytwo/jenkins/model/BuildWithDetailsTest.java @@ -2,6 +2,7 @@ import com.offbytwo.jenkins.BaseUnitTest; import com.offbytwo.jenkins.client.JenkinsHttpClient; +import com.offbytwo.jenkins.client.JenkinsHttpConnection; import com.offbytwo.jenkins.helper.BuildConsoleStreamListener; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; @@ -24,7 +25,7 @@ */ public class BuildWithDetailsTest extends BaseUnitTest { - private JenkinsHttpClient client; + private JenkinsHttpConnection client; private BuildWithDetails buildWithDetails; @Before