diff --git a/src/main/java/com/cdancy/jenkins/rest/domain/common/IntegerResponse.java b/src/main/java/com/cdancy/jenkins/rest/domain/common/IntegerResponse.java new file mode 100644 index 00000000..a8fe6367 --- /dev/null +++ b/src/main/java/com/cdancy/jenkins/rest/domain/common/IntegerResponse.java @@ -0,0 +1,46 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cdancy.jenkins.rest.domain.common; + +import java.util.List; + +import org.jclouds.javax.annotation.Nullable; +import org.jclouds.json.SerializedNames; + +import com.cdancy.jenkins.rest.JenkinsUtils; +import com.google.auto.value.AutoValue; + +/** + * Integer response to be returned when an endpoint returns + * an integer. + * + *

When the HTTP response code is valid the `value` parameter will + * be set to the integer value while a non-valid response has the `value` set to + * null along with any potential `error` objects returned from Jenkins. + */ +@AutoValue +public abstract class IntegerResponse implements Value, ErrorsHolder { + + @SerializedNames({ "value", "errors" }) + public static IntegerResponse create(@Nullable final Integer value, + final List errors) { + + return new AutoValue_IntegerResponse(value, + JenkinsUtils.nullToEmpty(errors)); + } +} diff --git a/src/main/java/com/cdancy/jenkins/rest/fallbacks/JenkinsFallbacks.java b/src/main/java/com/cdancy/jenkins/rest/fallbacks/JenkinsFallbacks.java index 08d88027..dfc8e874 100644 --- a/src/main/java/com/cdancy/jenkins/rest/fallbacks/JenkinsFallbacks.java +++ b/src/main/java/com/cdancy/jenkins/rest/fallbacks/JenkinsFallbacks.java @@ -22,6 +22,7 @@ import static org.jclouds.http.HttpUtils.returnValueOnCodeOrNull; +import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.cdancy.jenkins.rest.domain.common.Error; import com.cdancy.jenkins.rest.domain.crumb.Crumb; @@ -80,6 +81,20 @@ public Object createOrPropagate(final Throwable throwable) throws Exception { } } + public static final class IntegerResponseOnError implements Fallback { + @Override + public Object createOrPropagate(final Throwable throwable) throws Exception { + if (checkNotNull(throwable, "throwable") != null) { + try { + return IntegerResponse.create(null, getErrors(throwable)); + } catch (JsonSyntaxException e) { + return IntegerResponse.create(null, getErrors(e)); + } + } + throw propagate(throwable); + } + } + public static final class CrumbOnError implements Fallback { @Override public Object createOrPropagate(final Throwable throwable) throws Exception { diff --git a/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java b/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java index a909c99d..08053525 100644 --- a/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java +++ b/src/main/java/com/cdancy/jenkins/rest/features/JobsApi.java @@ -40,6 +40,7 @@ import org.jclouds.rest.annotations.ResponseParser; import com.cdancy.jenkins.rest.binders.BindMapToForm; +import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.cdancy.jenkins.rest.domain.job.BuildInfo; import com.cdancy.jenkins.rest.domain.job.JobInfo; @@ -137,19 +138,19 @@ boolean description(@PathParam("name") String jobName, @Named("jobs:build") @Path("/job/{name}/build") - @Fallback(Fallbacks.NullOnNotFoundOr404.class) + @Fallback(JenkinsFallbacks.IntegerResponseOnError.class) @ResponseParser(LocationToQueueId.class) @Consumes("application/unknown") @POST - Integer build(@PathParam("name") String jobName); + IntegerResponse build(@PathParam("name") String jobName); @Named("jobs:build-with-params") @Path("/job/{name}/buildWithParameters") - @Fallback(Fallbacks.NullOnNotFoundOr404.class) + @Fallback(JenkinsFallbacks.IntegerResponseOnError.class) @ResponseParser(LocationToQueueId.class) @Consumes("application/unknown") @POST - Integer buildWithParameters(@PathParam("name") String jobName, + IntegerResponse buildWithParameters(@PathParam("name") String jobName, @BinderParam(BindMapToForm.class) Map> properties); @Named("jobs:last-build-number") diff --git a/src/main/java/com/cdancy/jenkins/rest/parsers/LocationToQueueId.java b/src/main/java/com/cdancy/jenkins/rest/parsers/LocationToQueueId.java index e163f5a5..6e80159e 100644 --- a/src/main/java/com/cdancy/jenkins/rest/parsers/LocationToQueueId.java +++ b/src/main/java/com/cdancy/jenkins/rest/parsers/LocationToQueueId.java @@ -17,6 +17,7 @@ package com.cdancy.jenkins.rest.parsers; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,25 +26,31 @@ import org.jclouds.http.HttpResponse; import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import com.cdancy.jenkins.rest.domain.common.Error; +import com.cdancy.jenkins.rest.domain.common.IntegerResponse; /** * Created by dancc on 3/11/16. */ @Singleton -public class LocationToQueueId implements Function { +public class LocationToQueueId implements Function { private static final Pattern pattern = Pattern.compile("^.*/queue/item/(\\d+)/$"); - public Integer apply(HttpResponse response) { + public IntegerResponse apply(HttpResponse response) { String url = response.getFirstHeaderOrNull("Location"); if (url != null) { Matcher matcher = pattern.matcher(url); if (matcher.find() && matcher.groupCount() == 1) { - return Integer.valueOf(matcher.group(1)); + return IntegerResponse.create(Integer.valueOf(matcher.group(1)), null); } } - - return 0; + final Error error = Error.create(null, + "No queue item Location header could be found despite getting a valid HTTP response.", + NumberFormatException.class.getCanonicalName()); + return IntegerResponse.create(null, Lists.newArrayList(error)); } } diff --git a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java index 99e34f1c..9df342ba 100644 --- a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java +++ b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiLiveTest.java @@ -28,6 +28,7 @@ import org.testng.annotations.Test; import com.cdancy.jenkins.rest.BaseJenkinsApiLiveTest; +import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.cdancy.jenkins.rest.domain.job.BuildInfo; import com.cdancy.jenkins.rest.domain.job.JobInfo; @@ -38,7 +39,7 @@ @Test(groups = "live", testName = "SystemApiLiveTest", singleThreaded = true) public class JobsApiLiveTest extends BaseJenkinsApiLiveTest { - private Integer queueId; + private IntegerResponse queueId; private Integer buildNumber; @Test @@ -74,7 +75,8 @@ public void testLastBuildTimestampOnJobWithNoBuilds() { public void testBuildJob() { queueId = api().build("DevTest"); assertNotNull(queueId); - assertTrue(queueId > 0); + assertTrue(queueId.value() > 0); + assertTrue(queueId.errors().size() == 0); } @Test(dependsOnMethods = "testBuildJob") @@ -108,7 +110,7 @@ public void testGetBuildInfo() { BuildInfo output = api().buildInfo("DevTest", buildNumber); assertNotNull(output); assertTrue(output.fullDisplayName().equals("DevTest #" + buildNumber)); - assertTrue(output.queueId() == queueId); + assertTrue(output.queueId() == queueId.value()); } @Test(dependsOnMethods = "testGetBuildInfo") @@ -147,9 +149,10 @@ public void testUpdateConfig() { public void testBuildJobWithParameters() { Map> params = new HashMap<>(); params.put("SomeKey", Lists.newArrayList("SomeVeryNewValue")); - Integer output = api().buildWithParameters("DevTest", params); + IntegerResponse output = api().buildWithParameters("DevTest", params); assertNotNull(output); - assertTrue(output > 0); + assertTrue(output.value() > 0); + assertTrue(output.errors().size() == 0); } @Test(dependsOnMethods = "testBuildJobWithParameters") @@ -216,8 +219,13 @@ public void testGetDescriptionNonExistentJob() { @Test public void testBuildNonExistentJob() { - Integer output = api().build(randomString()); - assertNull(output); + IntegerResponse output = api().build(randomString()); + assertNotNull(output); + assertNull(output.value()); + assertTrue(output.errors().size() > 0); + assertNotNull(output.errors().get(0).context()); + assertNotNull(output.errors().get(0).message()); + assertTrue(output.errors().get(0).exceptionName().equals("org.jclouds.rest.ResourceNotFoundException")); } @Test @@ -230,8 +238,13 @@ public void testGetBuildInfoNonExistentJob() { public void testBuildNonExistentJobWithParams() { Map> params = new HashMap<>(); params.put("SomeKey", Lists.newArrayList("SomeVeryNewValue")); - Integer output = api().buildWithParameters(randomString(), params); - assertNull(output); + IntegerResponse output = api().buildWithParameters(randomString(), params); + assertNotNull(output); + assertNull(output.value()); + assertTrue(output.errors().size() > 0); + assertNotNull(output.errors().get(0).context()); + assertNotNull(output.errors().get(0).message()); + assertTrue(output.errors().get(0).exceptionName().equals("org.jclouds.rest.ResourceNotFoundException")); } private JobsApi api() { diff --git a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java index fb8258aa..69a30485 100644 --- a/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java +++ b/src/test/java/com/cdancy/jenkins/rest/features/JobsApiMockTest.java @@ -34,6 +34,7 @@ import com.cdancy.jenkins.rest.domain.job.JobInfo; import com.cdancy.jenkins.rest.domain.job.ProgressiveText; import com.cdancy.jenkins.rest.BaseJenkinsMockTest; +import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.google.common.collect.Lists; @@ -399,9 +400,10 @@ public void testBuildJob() throws Exception { JenkinsApi jenkinsApi = api(server.getUrl("/")); JobsApi api = jenkinsApi.jobsApi(); try { - Integer output = api.build("DevTest"); + IntegerResponse output = api.build("DevTest"); assertNotNull(output); - assertTrue(output == 1); + assertTrue(output.value() == 1); + assertTrue(output.errors().size() == 0); assertSentAccept(server, "POST", "/job/DevTest/build", "application/unknown"); } finally { jenkinsApi.close(); @@ -417,9 +419,13 @@ public void testBuildJobWithNoLocationReturned() throws Exception { JenkinsApi jenkinsApi = api(server.getUrl("/")); JobsApi api = jenkinsApi.jobsApi(); try { - Integer output = api.build("DevTest"); + IntegerResponse output = api.build("DevTest"); assertNotNull(output); - assertTrue(output == 0); + assertNull(output.value()); + assertTrue(output.errors().size() == 1); + assertNull(output.errors().get(0).context()); + assertTrue(output.errors().get(0).message().equals("No queue item Location header could be found despite getting a valid HTTP response.")); + assertTrue(output.errors().get(0).exceptionName().equals(NumberFormatException.class.getCanonicalName())); assertSentAccept(server, "POST", "/job/DevTest/build", "application/unknown"); } finally { jenkinsApi.close(); @@ -434,8 +440,13 @@ public void testBuildJobNonExistentJob() throws Exception { JenkinsApi jenkinsApi = api(server.getUrl("/")); JobsApi api = jenkinsApi.jobsApi(); try { - Integer output = api.build("DevTest"); - assertNull(output); + IntegerResponse output = api.build("DevTest"); + assertNotNull(output); + assertNull(output.value()); + assertTrue(output.errors().size() == 1); + assertTrue(output.errors().get(0).message().equals("")); + assertTrue(output.errors().get(0).exceptionName().equals("org.jclouds.rest.ResourceNotFoundException")); + assertNotNull(output.errors().get(0).context()); assertSentAccept(server, "POST", "/job/DevTest/build", "application/unknown"); } finally { jenkinsApi.close(); @@ -453,9 +464,10 @@ public void testBuildJobWithParams() throws Exception { try { Map> params = new HashMap<>(); params.put("SomeKey", Lists.newArrayList("SomeVeryNewValue")); - Integer output = api.buildWithParameters("DevTest", params); + IntegerResponse output = api.buildWithParameters("DevTest", params); assertNotNull(output); - assertTrue(output == 1); + assertTrue(output.value() == 1); + assertTrue(output.errors().size() == 0); assertSentAccept(server, "POST", "/job/DevTest/buildWithParameters", "application/unknown"); } finally { jenkinsApi.close(); @@ -472,8 +484,13 @@ public void testBuildJobWithParamsNonExistentJob() throws Exception { try { Map> params = new HashMap<>(); params.put("SomeKey", Lists.newArrayList("SomeVeryNewValue")); - Integer output = api.buildWithParameters("DevTest", params); - assertNull(output); + IntegerResponse output = api.buildWithParameters("DevTest", params); + assertNotNull(output); + assertNull(output.value()); + assertTrue(output.errors().size() == 1); + assertTrue(output.errors().get(0).message().equals("")); + assertTrue(output.errors().get(0).exceptionName().equals("org.jclouds.rest.ResourceNotFoundException")); + assertNotNull(output.errors().get(0).context()); assertSentAccept(server, "POST", "/job/DevTest/buildWithParameters", "application/unknown"); } finally { jenkinsApi.close(); diff --git a/src/test/java/com/cdancy/jenkins/rest/features/QueueApiLiveTest.java b/src/test/java/com/cdancy/jenkins/rest/features/QueueApiLiveTest.java index 3aea7c83..713edc88 100644 --- a/src/test/java/com/cdancy/jenkins/rest/features/QueueApiLiveTest.java +++ b/src/test/java/com/cdancy/jenkins/rest/features/QueueApiLiveTest.java @@ -31,6 +31,7 @@ import org.testng.annotations.Test; import com.cdancy.jenkins.rest.BaseJenkinsApiLiveTest; +import com.cdancy.jenkins.rest.domain.common.IntegerResponse; import com.cdancy.jenkins.rest.domain.common.RequestStatus; import com.cdancy.jenkins.rest.domain.queue.QueueItem; @@ -57,15 +58,17 @@ public void init() { @Test public void testGetQueue() { - Integer job1 = api.jobsApi().build("QueueTest"); + IntegerResponse job1 = api.jobsApi().build("QueueTest"); assertNotNull(job1); - Integer job2 = api.jobsApi().build("QueueTest"); + assertTrue(job1.errors().size() == 0); + IntegerResponse job2 = api.jobsApi().build("QueueTest"); assertNotNull(job2); + assertTrue(job2.errors().size() == 0); List queueItems = api().queue(); assertTrue(queueItems.size() > 0); boolean foundLastKickedJob = false; for (QueueItem item : queueItems) { - if (item.id() == job2) { + if (item.id() == job2.value()) { foundLastKickedJob = true; break; } @@ -75,13 +78,15 @@ public void testGetQueue() { @Test public void testGetPendingQueueItem() { - Integer job1 = api.jobsApi().build("QueueTest"); + IntegerResponse job1 = api.jobsApi().build("QueueTest"); assertNotNull(job1); - Integer job2 = api.jobsApi().build("QueueTest"); + assertTrue(job1.errors().size() == 0); + IntegerResponse job2 = api.jobsApi().build("QueueTest"); assertNotNull(job2); + assertTrue(job2.errors().size() == 0); // job2 is queue after job1, so while job1 runs, job2 is pending in the queue - QueueItem queueItem = api().queueItem(job2); + QueueItem queueItem = api().queueItem(job2.value()); assertFalse(queueItem.cancelled()); assertNotNull(queueItem.why()); assertNull(queueItem.executable()); @@ -89,13 +94,15 @@ public void testGetPendingQueueItem() { @Test public void testGetRunningQueueItem() throws InterruptedException { - Integer job1 = api.jobsApi().build("QueueTest"); + IntegerResponse job1 = api.jobsApi().build("QueueTest"); assertNotNull(job1); - Integer job2 = api.jobsApi().build("QueueTest"); + assertTrue(job1.errors().size() == 0); + IntegerResponse job2 = api.jobsApi().build("QueueTest"); assertNotNull(job2); + assertTrue(job2.errors().size() == 0); // job1 runs first, so we get its queueItem - QueueItem queueItem = getRunningQueueItem(job1); + QueueItem queueItem = getRunningQueueItem(job1.value()); // If null, it means the queueItem has been cancelled, which would not be normal in this test assertNotNull(queueItem); @@ -114,17 +121,21 @@ public void testGetRunningQueueItem() throws InterruptedException { public void testQueueItemSingleParameters() throws InterruptedException { Map> params = new HashMap<>(); params.put("SomeKey", Lists.newArrayList("SomeVeryNewValue1")); - Integer job1 = api.jobsApi().buildWithParameters("QueueTestSingleParam", params); + IntegerResponse job1 = api.jobsApi().buildWithParameters("QueueTestSingleParam", params); assertNotNull(job1); + assertTrue(job1.value() > 0); + assertTrue(job1.errors().size() == 0); // Jenkins will reject two consecutive build requests when the build parameter values are the same // So we must set some different parameter values params = new HashMap<>(); params.put("SomeKey", Lists.newArrayList("SomeVeryNewValue2")); - Integer job2 = api.jobsApi().buildWithParameters("QueueTestSingleParam", params); + IntegerResponse job2 = api.jobsApi().buildWithParameters("QueueTestSingleParam", params); assertNotNull(job2); + assertTrue(job2.value() > 0); + assertTrue(job2.errors().size() == 0); - QueueItem queueItem = getRunningQueueItem(job1); + QueueItem queueItem = getRunningQueueItem(job1.value()); assertNotNull(queueItem); assertFalse(queueItem.cancelled()); @@ -137,17 +148,21 @@ public void testQueueItemSingleParameters() throws InterruptedException { public void testQueueItemMultipleParameters() throws InterruptedException { Map> params = new HashMap<>(); params.put("SomeKey1", Lists.newArrayList("SomeVeryNewValue1")); - Integer job1 = api.jobsApi().buildWithParameters("QueueTestMultipleParams",params); + IntegerResponse job1 = api.jobsApi().buildWithParameters("QueueTestMultipleParams",params); assertNotNull(job1); + assertTrue(job1.value() > 0); + assertTrue(job1.errors().size() == 0); // Jenkins will reject two consecutive build requests when the build parameter values are the same // So we must set some different parameter values params = new HashMap<>(); params.put("SomeKey1", Lists.newArrayList("SomeVeryNewValue2")); - Integer job2 = api.jobsApi().buildWithParameters("QueueTestMultipleParams", params); + IntegerResponse job2 = api.jobsApi().buildWithParameters("QueueTestMultipleParams", params); assertNotNull(job2); + assertTrue(job2.value() > 0); + assertTrue(job2.errors().size() == 0); - QueueItem queueItem = getRunningQueueItem(job1); + QueueItem queueItem = getRunningQueueItem(job1.value()); assertNotNull(queueItem); assertFalse(queueItem.cancelled()); @@ -160,17 +175,19 @@ public void testQueueItemMultipleParameters() throws InterruptedException { @Test public void testGetCancelledQueueItem() throws InterruptedException { - Integer job1 = api.jobsApi().build("QueueTest"); + IntegerResponse job1 = api.jobsApi().build("QueueTest"); assertNotNull(job1); - Integer job2 = api.jobsApi().build("QueueTest"); + assertTrue(job1.errors().size() == 0); + IntegerResponse job2 = api.jobsApi().build("QueueTest"); assertNotNull(job2); + assertTrue(job2.errors().size() == 0); - RequestStatus success = api().cancel(job2); + RequestStatus success = api().cancel(job2.value()); assertNotNull(success); assertTrue(success.value()); assertTrue(success.errors().isEmpty()); - QueueItem queueItem = api().queueItem(job2); + QueueItem queueItem = api().queueItem(job2.value()); assertTrue(queueItem.cancelled()); assertNull(queueItem.why()); assertNull(queueItem.executable());