diff --git a/core/src/main/java/org/jclouds/rest/binders/BindMapToMatrixParams.java b/core/src/main/java/org/jclouds/rest/binders/BindMapToMatrixParams.java new file mode 100644 index 0000000000..94735b9f08 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/binders/BindMapToMatrixParams.java @@ -0,0 +1,56 @@ +/** + * + * Copyright (C) 2009 Global Cloud Specialists, Inc. + * + * ==================================================================== + * 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 org.jclouds.rest.binders; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; +import java.util.Map.Entry; + +import org.jclouds.http.HttpRequest; +import org.jclouds.rest.Binder; +import org.jclouds.rest.internal.GeneratedHttpRequest; + +/** + * Binds the map to matrix parameters. + * + * @author Adrian Cole + * @since 4.0 + */ +public class BindMapToMatrixParams implements Binder { + + @SuppressWarnings("unchecked") + public void bindToRequest(HttpRequest request, Object input) { + checkArgument(checkNotNull(request, "input") instanceof GeneratedHttpRequest, + "this binder is only valid for GeneratedHttpRequests!"); + checkArgument(checkNotNull(input, "input") instanceof Map, + "this binder is only valid for Maps!"); + Map map = (Map) input; + for (Entry entry : map.entrySet()) { + ((GeneratedHttpRequest) request).replaceMatrixParam(entry.getKey(), entry.getValue()); + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/internal/GeneratedHttpRequest.java b/core/src/main/java/org/jclouds/rest/internal/GeneratedHttpRequest.java index edf45b28e8..93894d916c 100644 --- a/core/src/main/java/org/jclouds/rest/internal/GeneratedHttpRequest.java +++ b/core/src/main/java/org/jclouds/rest/internal/GeneratedHttpRequest.java @@ -66,6 +66,12 @@ public class GeneratedHttpRequest extends HttpRequest { return processor; } + public void replaceMatrixParam(String name, Object... values) { + UriBuilder builder = UriBuilder.fromUri(getEndpoint()); + builder.replaceMatrixParam(name, values); + replacePath(builder.build().getPath()); + } + public void replaceQueryParam(String name, Object... values) { UriBuilder builder = UriBuilder.fromUri(getEndpoint()); builder.replaceQueryParam(name, values); diff --git a/core/src/test/java/org/jclouds/http/BackoffLimitedRetryJavaIntegrationTest.java b/core/src/test/java/org/jclouds/http/BackoffLimitedRetryJavaIntegrationTest.java index 6dbf74dd29..779ffad2e4 100644 --- a/core/src/test/java/org/jclouds/http/BackoffLimitedRetryJavaIntegrationTest.java +++ b/core/src/test/java/org/jclouds/http/BackoffLimitedRetryJavaIntegrationTest.java @@ -66,7 +66,7 @@ public class BackoffLimitedRetryJavaIntegrationTest extends BaseJettyTest { } @Override - protected boolean failOnRequest(HttpServletRequest request, HttpServletResponse response) + protected boolean failEveryTenRequests(HttpServletRequest request, HttpServletResponse response) throws IOException { requestCount++; boolean shouldFail = requestCount >= beginToFailOnRequestNumber diff --git a/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceTest.java b/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceTest.java index 81b96cfe25..3301dd3b58 100644 --- a/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceTest.java +++ b/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceTest.java @@ -30,11 +30,15 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; import org.jclouds.http.options.GetOptions; +import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import com.google.common.collect.ImmutableMap; + /** * Tests for functionality all HttpCommandExecutorServices must express. These tests will operate * against an in-memory http engine, so as to ensure end-to-end functionality works. @@ -117,6 +121,30 @@ public abstract class BaseHttpCommandExecutorServiceTest extends BaseJettyTest { assertEquals(put.get(10, TimeUnit.SECONDS).trim(), "fooPOST"); } + @Test(invocationCount = 50, timeOut = 10000) + public void testPostAsInputStream() throws MalformedURLException, ExecutionException, + InterruptedException, TimeoutException { + try { + Future put = client.postAsInputStream("", "foo"); + assertEquals(put.get(10, TimeUnit.SECONDS).trim(), "fooPOST"); + } catch (Exception e) { + postFailures.incrementAndGet(); + } + } + + protected AtomicInteger postFailures = new AtomicInteger(); + + @BeforeTest + void resetCounters() { + postFailures.set(0); + } + + @Test(dependsOnMethods = "testPostAsInputStream") + public void testPostResults() { + // failures happen when trying to replay inputstreams + assert postFailures.get() > 0; + } + @Test(invocationCount = 50, timeOut = 5000) public void testPostBinder() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { @@ -138,6 +166,13 @@ public abstract class BaseHttpCommandExecutorServiceTest extends BaseJettyTest { assertEquals(put.get(10, TimeUnit.SECONDS).trim(), "fooPUTREDIRECT"); } + @Test(invocationCount = 50, timeOut = 5000) + public void testKillRobotSlowly() throws MalformedURLException, ExecutionException, + InterruptedException, TimeoutException { + Future dead = client.action("robot", "kill", ImmutableMap.of("death", "slow")); + assertEquals(dead.get(10, TimeUnit.SECONDS).trim(), "robot->kill:{death=slow}"); + } + @Test(invocationCount = 50, timeOut = 5000) public void testHead() throws MalformedURLException, ExecutionException, InterruptedException, TimeoutException { diff --git a/core/src/test/java/org/jclouds/http/BaseJettyTest.java b/core/src/test/java/org/jclouds/http/BaseJettyTest.java index 602be87080..b7dbe43432 100644 --- a/core/src/test/java/org/jclouds/http/BaseJettyTest.java +++ b/core/src/test/java/org/jclouds/http/BaseJettyTest.java @@ -26,8 +26,11 @@ package org.jclouds.http; import java.io.IOException; import java.net.URI; import java.util.List; +import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.inject.Singleton; import javax.servlet.ServletException; @@ -53,6 +56,7 @@ import org.testng.annotations.BeforeTest; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; +import com.google.common.collect.Maps; import com.google.inject.AbstractModule; import com.google.inject.Injector; import com.google.inject.Module; @@ -111,10 +115,13 @@ public abstract class BaseJettyTest { private AtomicInteger cycle = new AtomicInteger(0); private Server server2; private CloudContext context; + private int testPort; + static final Pattern actionPattern = Pattern.compile("/objects/(.*)/action/([a-z]*);?(.*)"); @BeforeTest @Parameters( { "test-jetty-port" }) public void setUpJetty(@Optional("8123") final int testPort) throws Exception { + this.testPort = testPort; Handler server1Handler = new AbstractHandler() { public void handle(String target, HttpServletRequest request, HttpServletResponse response, int dispatch) throws IOException, ServletException { @@ -131,12 +138,16 @@ public abstract class BaseJettyTest { response.sendError(500, "no content"); } } else if (request.getMethod().equals("POST")) { + if (redirectEveryTwentyRequests(request, response)) + return; + if (failEveryTenRequests(request, response)) + return; if (request.getContentLength() > 0) { response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println( Utils.toStringAndClose(request.getInputStream()) + "POST"); } else { - response.sendError(500, "no content"); + handleAction(request, response); } } else if (request.getHeader("Range") != null) { response.sendError(404, "no content"); @@ -145,7 +156,7 @@ public abstract class BaseJettyTest { response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println("test"); } else { - if (failOnRequest(request, response)) + if (failEveryTenRequests(request, response)) return; response.setContentType("text/xml"); response.setStatus(HttpServletResponse.SC_OK); @@ -153,6 +164,7 @@ public abstract class BaseJettyTest { } ((Request) request).setHandled(true); } + }; server = new Server(testPort); @@ -167,8 +179,14 @@ public abstract class BaseJettyTest { response.setStatus(HttpServletResponse.SC_OK); response.getWriter().println( Utils.toStringAndClose(request.getInputStream()) + "PUTREDIRECT"); + } + } else if (request.getMethod().equals("POST")) { + if (request.getContentLength() > 0) { + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().println( + Utils.toStringAndClose(request.getInputStream()) + "POST"); } else { - response.sendError(500, "no content"); + handleAction(request, response); } } else { response.setContentType("text/xml"); @@ -227,7 +245,7 @@ public abstract class BaseJettyTest { * @return * @throws IOException */ - protected boolean failOnRequest(HttpServletRequest request, HttpServletResponse response) + protected boolean failEveryTenRequests(HttpServletRequest request, HttpServletResponse response) throws IOException { if (cycle.incrementAndGet() % 10 == 0) { response.sendError(500); @@ -237,6 +255,16 @@ public abstract class BaseJettyTest { return false; } + protected boolean redirectEveryTwentyRequests(HttpServletRequest request, + HttpServletResponse response) throws IOException { + if (cycle.incrementAndGet() % 20 == 0) { + response.sendRedirect("http://localhost:" + (testPort + 1)); + ((Request) request).setHandled(true); + return true; + } + return false; + } + protected boolean failIfNoContentLength(HttpServletRequest request, HttpServletResponse response) throws IOException { if (request.getHeader(HttpHeaders.CONTENT_LENGTH) == null) { @@ -247,4 +275,27 @@ public abstract class BaseJettyTest { return false; } + private void handleAction(HttpServletRequest request, HttpServletResponse response) + throws IOException { + final Matcher matcher = actionPattern.matcher(request.getRequestURI()); + boolean matchFound = matcher.find(); + if (matchFound) { + String objectId = matcher.group(1); + String action = matcher.group(2); + Map options = Maps.newHashMap(); + if (matcher.groupCount() == 3) { + String optionsGroup = matcher.group(3); + for (String entry : optionsGroup.split(";")) { + if (entry.indexOf('=') >= 0) { + String[] keyValue = entry.split("="); + options.put(keyValue[0], keyValue[1]); + } + } + } + response.setStatus(HttpServletResponse.SC_OK); + response.getWriter().println(objectId + "->" + action + ":" + options); + } else { + response.sendError(500, "no content"); + } + } } diff --git a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java index 0c2df54364..962c471f02 100644 --- a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java +++ b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java @@ -23,6 +23,7 @@ */ package org.jclouds.http; +import java.util.Map; import java.util.concurrent.Future; import javax.ws.rs.GET; @@ -33,6 +34,7 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import org.apache.commons.io.IOUtils; import org.jclouds.http.functions.ParseSax; import org.jclouds.http.options.HttpRequestOptions; import org.jclouds.rest.annotations.BinderParam; @@ -42,6 +44,7 @@ import org.jclouds.rest.annotations.MapBinder; import org.jclouds.rest.annotations.MapEntityParam; import org.jclouds.rest.annotations.RequestFilters; import org.jclouds.rest.annotations.XMLResponseParser; +import org.jclouds.rest.binders.BindMapToMatrixParams; import org.jclouds.rest.binders.BindToJsonEntity; import org.jclouds.rest.binders.BindToStringEntity; import org.jclouds.rest.internal.RestAnnotationProcessorTest.Localhost; @@ -96,10 +99,28 @@ public interface IntegrationTestClient { Future post(@PathParam("id") String id, @BinderParam(BindToStringEntity.class) String toPut); + @POST + @Path("objects/{id}") + Future postAsInputStream(@PathParam("id") String id, + @BinderParam(BindToInputStreamEntity.class) String toPut); + + static class BindToInputStreamEntity extends BindToStringEntity { + @Override + public void bindToRequest(HttpRequest request, Object entity) { + super.bindToRequest(request, entity); + request.setEntity(IOUtils.toInputStream(entity.toString())); + } + } + @POST @Path("objects/{id}") @MapBinder(BindToJsonEntity.class) Future postJson(@PathParam("id") String id, @MapEntityParam("key") String toPut); + + @POST + @Path("objects/{id}/action/{action}") + Future action(@PathParam("id") String id, @PathParam("action") String action, + @BinderParam(BindMapToMatrixParams.class) Map options); @GET @Path("objects/{id}") diff --git a/core/src/test/java/org/jclouds/rest/binders/BindMapToMatrixParamsTest.java b/core/src/test/java/org/jclouds/rest/binders/BindMapToMatrixParamsTest.java new file mode 100644 index 0000000000..7ee1cae92c --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/binders/BindMapToMatrixParamsTest.java @@ -0,0 +1,54 @@ +package org.jclouds.rest.binders; + +import static org.easymock.classextension.EasyMock.createMock; +import static org.easymock.classextension.EasyMock.replay; + +import java.io.File; +import java.net.URI; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.ext.RuntimeDelegate; + +import org.jclouds.http.HttpRequest; +import org.jclouds.rest.internal.GeneratedHttpRequest; +import org.jclouds.rest.internal.RuntimeDelegateImpl; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableMap; + +/** + * Tests behavior of {@code BindMapToMatrixParams} + * + * @author Adrian Cole + */ +@Test(groups = "unit", testName = "rest.BindMapToMatrixParamsTest") +public class BindMapToMatrixParamsTest { + static { + RuntimeDelegate.setInstance(new RuntimeDelegateImpl()); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testMustBeMap() { + BindMapToMatrixParams binder = new BindMapToMatrixParams(); + HttpRequest request = new HttpRequest(HttpMethod.POST, URI.create("http://localhost")); + binder.bindToRequest(request, new File("foo")); + } + + @Test + public void testCorrect() throws SecurityException, NoSuchMethodException { + BindMapToMatrixParams binder = new BindMapToMatrixParams(); + + GeneratedHttpRequest request = createMock(GeneratedHttpRequest.class); + request.replaceMatrixParam("imageName", "foo"); + request.replaceMatrixParam("serverId", "2"); + replay(request); + binder.bindToRequest(request, ImmutableMap.of("imageName", "foo", "serverId", "2")); + } + + @Test(expectedExceptions = { NullPointerException.class, IllegalStateException.class }) + public void testNullIsBad() { + BindMapToMatrixParams binder = new BindMapToMatrixParams(); + GeneratedHttpRequest request = createMock(GeneratedHttpRequest.class); + binder.bindToRequest(request, null); + } +} diff --git a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java index 1f739f9429..8cee23330c 100755 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -87,6 +87,7 @@ import org.jclouds.rest.annotations.RequestFilters; import org.jclouds.rest.annotations.ResponseParser; import org.jclouds.rest.annotations.SkipEncoding; import org.jclouds.rest.annotations.VirtualHost; +import org.jclouds.rest.binders.BindMapToMatrixParams; import org.jclouds.rest.binders.BindToJsonEntity; import org.jclouds.rest.binders.BindToStringEntity; import org.jclouds.rest.config.RestModule; @@ -100,6 +101,7 @@ import org.testng.annotations.Test; import com.google.common.base.Function; import com.google.common.collect.HashMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.Multimap; import com.google.inject.AbstractModule; @@ -739,6 +741,24 @@ public class RestAnnotationProcessorTest { assertEquals(query, "x-amz-copy-source=/robot"); } + @Endpoint(Localhost.class) + private interface TestMapMatrixParams { + @POST + @Path("objects/{id}/action/{action}") + Future action(@PathParam("id") String id, @PathParam("action") String action, + @BinderParam(BindMapToMatrixParams.class) Map options); + } + + public void testTestMapMatrixParams() throws SecurityException, NoSuchMethodException, + UnsupportedEncodingException { + Method method = TestMapMatrixParams.class.getMethod("action", String.class, String.class, + Map.class); + GeneratedHttpRequest httpMethod = factory(TestMapMatrixParams.class).createRequest(method, + new Object[] { "robot", "kill", ImmutableMap.of("death", "slow") }); + assertEquals(httpMethod.getRequestLine(), "POST http://localhost:8080/objects/robot/action/kill;death=slow HTTP/1.1"); + assertEquals(httpMethod.getHeaders().size(), 0); + } + @Endpoint(Localhost.class) public class TestQueryReplace { diff --git a/extensions/gae/src/test/java/org/jclouds/gae/GaeHttpCommandExecutorServiceIntegrationTest.java b/extensions/gae/src/test/java/org/jclouds/gae/GaeHttpCommandExecutorServiceIntegrationTest.java index 75927bd520..b4671b2f54 100644 --- a/extensions/gae/src/test/java/org/jclouds/gae/GaeHttpCommandExecutorServiceIntegrationTest.java +++ b/extensions/gae/src/test/java/org/jclouds/gae/GaeHttpCommandExecutorServiceIntegrationTest.java @@ -23,6 +23,8 @@ */ package org.jclouds.gae; +import static org.testng.Assert.assertEquals; + import java.io.File; import java.net.MalformedURLException; import java.util.Map; @@ -51,6 +53,29 @@ import com.google.inject.Module; public class GaeHttpCommandExecutorServiceIntegrationTest extends BaseHttpCommandExecutorServiceTest { + @Override + @Test(invocationCount = 50, timeOut = 3000) + public void testKillRobotSlowly() throws MalformedURLException, ExecutionException, + InterruptedException, TimeoutException { + setupApiProxy(); + super.testKillRobotSlowly(); + } + + @Override + @Test(invocationCount = 50, timeOut = 3000) + public void testPostAsInputStream() throws MalformedURLException, ExecutionException, + InterruptedException, TimeoutException { + setupApiProxy(); + super.testPostAsInputStream(); + } + + @Override + @Test(dependsOnMethods = "testPostAsInputStream") + public void testPostResults() { + // GAE converts everything to byte arrays and so failures are not gonna happen + assertEquals(postFailures.get(), 0); + } + @Override @Test(invocationCount = 50, timeOut = 3000) public void testPostBinder() throws MalformedURLException, ExecutionException, @@ -61,10 +86,11 @@ public class GaeHttpCommandExecutorServiceIntegrationTest extends @BeforeTest void validateExecutor() { -// ExecutorService executorService = injector.getInstance(ExecutorService.class); -// assert executorService.getClass().isAnnotationPresent(SingleThreadCompatible.class) : Arrays -// .asList(executorService.getClass().getAnnotations()).toString() -// + executorService.getClass().getName(); + // ExecutorService executorService = injector.getInstance(ExecutorService.class); + // assert executorService.getClass().isAnnotationPresent(SingleThreadCompatible.class) : + // Arrays + // .asList(executorService.getClass().getAnnotations()).toString() + // + executorService.getClass().getName(); } diff --git a/extensions/httpnio/src/test/java/org/jclouds/http/httpnio/pool/NioTransformingHttpCommandExecutorServiceTest.java b/extensions/httpnio/src/test/java/org/jclouds/http/httpnio/pool/NioTransformingHttpCommandExecutorServiceTest.java index bf79ba693e..7654372f0d 100644 --- a/extensions/httpnio/src/test/java/org/jclouds/http/httpnio/pool/NioTransformingHttpCommandExecutorServiceTest.java +++ b/extensions/httpnio/src/test/java/org/jclouds/http/httpnio/pool/NioTransformingHttpCommandExecutorServiceTest.java @@ -23,7 +23,10 @@ */ package org.jclouds.http.httpnio.pool; +import java.net.MalformedURLException; import java.util.Properties; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; import org.jclouds.http.BaseHttpCommandExecutorServiceTest; import org.jclouds.http.httpnio.config.NioTransformingHttpCommandExecutorServiceModule; @@ -40,6 +43,19 @@ import com.google.inject.Module; public class NioTransformingHttpCommandExecutorServiceTest extends BaseHttpCommandExecutorServiceTest { + @Override + @Test(enabled = false) + public void testPostAsInputStream() throws MalformedURLException, ExecutionException, + InterruptedException, TimeoutException { + // TODO when these fail, we hang + } + + @Override + @Test(enabled = false) + public void testPostResults() { + // see above + } + protected Module createClientModule() { return new NioTransformingHttpCommandExecutorServiceModule(); }