diff --git a/labs/dynect/pom.xml b/labs/dynect/pom.xml index 3cc55b91a3..c2fc25291f 100644 --- a/labs/dynect/pom.xml +++ b/labs/dynect/pom.xml @@ -61,6 +61,11 @@ test-jar test + + com.google.mockwebserver + mockwebserver + test + org.jclouds.driver jclouds-slf4j diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTProviderMetadata.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTProviderMetadata.java index c1c3a90fea..502726ff56 100644 --- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTProviderMetadata.java +++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/DynECTProviderMetadata.java @@ -19,6 +19,7 @@ package org.jclouds.dynect.v3; import static org.jclouds.Constants.PROPERTY_MAX_REDIRECTS; +import static org.jclouds.Constants.PROPERTY_RETRY_DELAY_START; import java.net.URI; import java.util.Properties; @@ -51,8 +52,9 @@ public class DynECTProviderMetadata extends BaseProviderMetadata { public static Properties defaultProperties() { Properties properties = new Properties(); - // job polling occurs via redirect loop - properties.setProperty(PROPERTY_MAX_REDIRECTS, "20"); + // job polling occurs via redirect loop and can take a while + properties.setProperty(PROPERTY_MAX_REDIRECTS, "100"); + properties.setProperty(PROPERTY_RETRY_DELAY_START, "200"); return properties; } diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java index 770ed7dbf1..2cdd2a609e 100644 --- a/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java +++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java @@ -18,10 +18,23 @@ */ package org.jclouds.dynect.v3.config; +import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream; import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.Proxy; +import java.net.URI; import java.util.Map; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; + +import org.jclouds.Constants; +import org.jclouds.concurrent.SingleThreaded; import org.jclouds.dynect.v3.DynECTApi; import org.jclouds.dynect.v3.DynECTAsyncApi; import org.jclouds.dynect.v3.features.SessionApi; @@ -29,14 +42,29 @@ import org.jclouds.dynect.v3.features.SessionAsyncApi; import org.jclouds.dynect.v3.features.ZoneApi; import org.jclouds.dynect.v3.features.ZoneAsyncApi; import org.jclouds.dynect.v3.filters.SessionManager; +import org.jclouds.dynect.v3.handlers.DynECTErrorHandler; import org.jclouds.dynect.v3.handlers.GetJobRedirectionRetryHandler; +import org.jclouds.http.HttpErrorHandler; +import org.jclouds.http.HttpResponse; import org.jclouds.http.HttpRetryHandler; +import org.jclouds.http.HttpUtils; +import org.jclouds.http.IOExceptionRetryHandler; import org.jclouds.http.annotation.ClientError; +import org.jclouds.http.annotation.Redirection; +import org.jclouds.http.annotation.ServerError; +import org.jclouds.http.handlers.DelegatingErrorHandler; +import org.jclouds.http.handlers.DelegatingRetryHandler; import org.jclouds.http.handlers.RedirectionRetryHandler; +import org.jclouds.http.internal.HttpWire; +import org.jclouds.http.internal.JavaUrlHttpCommandExecutorService; +import org.jclouds.io.ContentMetadataCodec; import org.jclouds.rest.ConfiguresRestClient; import org.jclouds.rest.config.RestClientModule; +import com.google.common.base.Function; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.ListeningExecutorService; /** * Configures the DynECT connection. @@ -44,6 +72,8 @@ import com.google.common.collect.ImmutableMap; * @author Adrian Cole */ @ConfiguresRestClient +// only one job at a time or error "This session already has a job running" +@SingleThreaded public class DynECTRestClientModule extends RestClientModule { public static final Map, Class> DELEGATE_MAP = ImmutableMap., Class> builder() @@ -54,6 +84,13 @@ public class DynECTRestClientModule extends RestClientModule untrustedSSLContextProvider, Function proxyForURI) + throws SecurityException, NoSuchFieldException { + super(utils, contentMetadataCodec, ioExecutor, retryHandler, ioRetryHandler, errorHandler, wire, verifier, + untrustedSSLContextProvider, proxyForURI); + } + + @Override + protected HttpResponse invoke(HttpURLConnection connection) throws IOException, InterruptedException { + HttpResponse response = super.invoke(connection); + if (response.getStatusCode() == 200) { + byte[] data = closeClientButKeepContentStream(response); + String message = data != null ? new String(data, "UTF-8") : null; + if (message != null && !message.startsWith("{\"status\": \"success\"")) { + response = response.toBuilder().statusCode(400).build(); + } + } + return response; + } + } } diff --git a/labs/dynect/src/main/java/org/jclouds/dynect/v3/handlers/DynECTErrorHandler.java b/labs/dynect/src/main/java/org/jclouds/dynect/v3/handlers/DynECTErrorHandler.java new file mode 100644 index 0000000000..b2f4afdabb --- /dev/null +++ b/labs/dynect/src/main/java/org/jclouds/dynect/v3/handlers/DynECTErrorHandler.java @@ -0,0 +1,52 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 org.jclouds.dynect.v3.handlers; + +import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream; +import static org.jclouds.http.HttpUtils.releasePayload; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpErrorHandler; +import org.jclouds.http.HttpResponse; +import org.jclouds.http.HttpResponseException; + +/** + * @author Adrian Cole + */ +public class DynECTErrorHandler implements HttpErrorHandler { + private static final String JOB_STILL_RUNNING = "This session already has a job running"; + + public void handleError(HttpCommand command, HttpResponse response) { + Exception exception = new HttpResponseException(command, response); + try { + byte[] data = closeClientButKeepContentStream(response); + String message = data != null ? new String(data) : null; + if (message != null) { + exception = new HttpResponseException(command, response, message); + if (message.indexOf(JOB_STILL_RUNNING) != -1) + exception = new IllegalStateException(JOB_STILL_RUNNING, exception); + } else { + exception = new HttpResponseException(command, response); + } + } finally { + releasePayload(response); + command.setException(exception); + } + } +} diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/DynectApiMockTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/DynectApiMockTest.java new file mode 100644 index 0000000000..09f53bb8cc --- /dev/null +++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/DynectApiMockTest.java @@ -0,0 +1,59 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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 org.jclouds.dynect.v3; + +import java.io.IOException; + +import org.jclouds.ContextBuilder; +import org.jclouds.rest.RestContext; +import org.testng.annotations.Test; + +import com.google.mockwebserver.MockResponse; +import com.google.mockwebserver.MockWebServer; + +/** + * + * @author Adrian Cole + */ +@Test +public class DynectApiMockTest { + + static RestContext getContext(String uri) { + return ContextBuilder.newBuilder("dynect").credentials("jclouds:joe", "letmein").endpoint(uri).build(); + } + + String session = "{\"status\": \"success\", \"data\": {\"token\": \"FFFFFFFFFF\", \"version\": \"3.3.7\"}, \"job_id\": 254417252, \"msgs\": [{\"INFO\": \"login: Login successful\", \"SOURCE\": \"BLL\", \"ERR_CD\": null, \"LVL\": \"INFO\"}]}"; + String failure = "{\"status\": \"failure\", \"data\": {}, \"job_id\": 274509427, \"msgs\": [{\"INFO\": \"token: This session already has a job running\", \"SOURCE\": \"API-B\", \"ERR_CD\": \"OPERATION_FAILED\", \"LVL\": \"ERROR\"}]}"; + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "This session already has a job running") + public void test200OnFailureThrowsExceptionWithoutRetry() throws IOException, InterruptedException { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setResponseCode(200).setBody(session)); + server.enqueue(new MockResponse().setResponseCode(200).setBody(failure)); + server.play(); + + DynECTApi api = getContext(server.getUrl("/").toString()).getApi(); + + try { + api.getZoneApi().list(); + } finally { + server.shutdown(); + } + } +} diff --git a/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java b/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java index bafed021ee..6697af88e7 100644 --- a/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java +++ b/labs/dynect/src/test/java/org/jclouds/dynect/v3/internal/BaseDynECTExpectTest.java @@ -20,24 +20,43 @@ package org.jclouds.dynect.v3.internal; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import org.jclouds.dynect.v3.config.DynECTRestClientModule; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; +import org.jclouds.http.config.SSLModule; import org.jclouds.io.Payload; import org.jclouds.io.Payloads; +import org.jclouds.rest.ConfiguresRestClient; import org.jclouds.rest.internal.BaseRestApiExpectTest; +import com.google.inject.Module; + /** * Base class for writing DynECT Expect tests * * @author Adrian Cole */ public class BaseDynECTExpectTest extends BaseRestApiExpectTest { - public BaseDynECTExpectTest() { + public BaseDynECTExpectTest() { provider = "dynect"; identity = "jclouds:joe"; credential = "letmein"; } + @Override + protected Module createModule() { + return new TestDynECTRestClientModule(); + } + + @ConfiguresRestClient + private static final class TestDynECTRestClientModule extends DynECTRestClientModule { + @Override + protected void configure() { + install(new SSLModule()); + super.configure(); + } + } + public static Payload emptyJsonPayload() { Payload p = Payloads.newByteArrayPayload(new byte[] {}); p.getContentMetadata().setContentType(APPLICATION_JSON); @@ -57,12 +76,16 @@ public class BaseDynECTExpectTest extends BaseRestApiExpectTest { protected String authToken = "FFFFFFFFFF"; - protected HttpRequest createSession = HttpRequest.builder().method("POST") + protected HttpRequest createSession = HttpRequest + .builder() + .method("POST") .endpoint("https://api2.dynect.net/REST/Session") .addHeader("API-Version", "3.3.7") - .payload(payloadFromStringWithContentType("{\"customer_name\":\"jclouds\",\"user_name\":\"joe\",\"password\":\"letmein\"}",APPLICATION_JSON)) + .payload( + payloadFromStringWithContentType( + "{\"customer_name\":\"jclouds\",\"user_name\":\"joe\",\"password\":\"letmein\"}", APPLICATION_JSON)) .build(); - + protected HttpResponse createSessionResponse = HttpResponse.builder().statusCode(200) .payload(payloadFromResourceWithContentType("/create_session.json", APPLICATION_JSON)).build();