diff --git a/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java b/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java index 125e0e187b..bc866eadcb 100644 --- a/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java +++ b/core/src/main/java/org/jclouds/http/internal/JavaUrlHttpCommandExecutorService.java @@ -247,8 +247,10 @@ public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorSe } else { connection.setRequestProperty(CONTENT_LENGTH, "0"); // for some reason POST/PUT undoes the content length header above. - if (ImmutableSet.of("POST", "PUT").contains(connection.getRequestMethod())) + if (ImmutableSet.of("POST", "PUT").contains(connection.getRequestMethod())) { connection.setFixedLengthStreamingMode(0); + connection.setDoOutput(true); + } } return connection; } diff --git a/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceIntegrationTest.java b/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceIntegrationTest.java index d388d9ea53..8dc836a82b 100644 --- a/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceIntegrationTest.java +++ b/core/src/test/java/org/jclouds/http/BaseHttpCommandExecutorServiceIntegrationTest.java @@ -278,4 +278,9 @@ public abstract class BaseHttpCommandExecutorServiceIntegrationTest extends Base TimeoutException { assertEquals(client.downloadAndParse(""), "whoppers"); } + + @Test(invocationCount = 5, timeOut = 5000) + public void testZeroLengthPut() { + client.putNothing(""); + } } diff --git a/core/src/test/java/org/jclouds/http/BaseJettyTest.java b/core/src/test/java/org/jclouds/http/BaseJettyTest.java index 8dcbc6c425..e5a60685be 100644 --- a/core/src/test/java/org/jclouds/http/BaseJettyTest.java +++ b/core/src/test/java/org/jclouds/http/BaseJettyTest.java @@ -18,8 +18,8 @@ */ package org.jclouds.http; -import static com.google.common.base.Throwables.propagate; -import static com.google.common.collect.Maps.newHashMap; +import static com.google.common.base.Throwables.getStackTraceAsString; +import static com.google.common.hash.Hashing.md5; import static com.google.common.io.ByteStreams.copy; import static com.google.common.io.ByteStreams.join; import static com.google.common.io.ByteStreams.newInputStreamSupplier; @@ -40,8 +40,8 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Enumeration; -import java.util.Map; import java.util.Properties; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -50,7 +50,6 @@ import java.util.zip.GZIPInputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.HttpHeaders; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Handler; @@ -59,19 +58,19 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.ssl.SslSelectChannelConnector; import org.eclipse.jetty.util.ssl.SslContextFactory; -import org.jclouds.Constants; import org.jclouds.ContextBuilder; import org.jclouds.crypto.CryptoStreams; import org.jclouds.io.InputSuppliers; import org.jclouds.providers.AnonymousProviderMetadata; import org.jclouds.rest.RestContext; -import org.jclouds.util.Strings2; -import org.testng.annotations.AfterTest; -import org.testng.annotations.BeforeTest; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Optional; import org.testng.annotations.Parameters; -import com.google.common.base.Throwables; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableSet; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; @@ -94,80 +93,72 @@ public abstract class BaseJettyTest { protected String md5; static final Pattern actionPattern = Pattern.compile("/objects/(.*)/action/([a-z]*);?(.*)"); - @BeforeTest - @Parameters( { "test-jetty-port" }) + @BeforeClass + @Parameters({ "test-jetty-port" }) public void setUpJetty(@Optional("8123") final int testPort) throws Exception { this.testPort = testPort; final InputSupplier oneHundredOneConstitutions = getTestDataSupplier(); - md5 = CryptoStreams.md5Base64(oneHundredOneConstitutions); Handler server1Handler = new AbstractHandler() { public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - InputStream body = request.getInputStream(); - try { - if (failIfNoContentLength(request, response)) { - return; - } else if (target.indexOf("sleep") > 0) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - propagate(e); - } - response.setContentType("text/xml"); - response.setStatus(HttpServletResponse.SC_OK); - } else if (target.indexOf("redirect") > 0) { - response.sendRedirect("https://localhost:" + (testPort + 1) + "/"); - } else if (target.indexOf("101constitutions") > 0) { - response.setContentType("text/plain"); - response.setHeader("Content-MD5", md5); - response.setStatus(HttpServletResponse.SC_OK); - copy(oneHundredOneConstitutions.getInput(), response.getOutputStream()); - } else if (request.getMethod().equals("PUT")) { - if (request.getContentLength() > 0) { - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(Strings2.toStringAndClose(body) + "PUT"); - } else { - response.sendError(500, "no content"); - } - } else if (request.getMethod().equals("POST")) { - // don't redirect large objects - if (request.getContentLength() < 10240 && redirectEveryTwentyRequests(request, response)) - return; - if (failEveryTenRequests(request, response)) - return; - if (request.getContentLength() > 0) { - handlePost(request, response); - } else { - handleAction(request, response); - } - } else if (request.getHeader("range") != null) { - response.sendError(404, "no content"); - } else if (request.getHeader("test") != null) { - response.setContentType("text/plain"); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println("test"); - } else if (request.getMethod().equals("HEAD")) { - /* - * NOTE: by HTML specification, HEAD response MUST NOT include a body - */ - response.setContentType("text/xml"); - response.setStatus(HttpServletResponse.SC_OK); + throws IOException, ServletException { + if (failIfNoContentLength(request, response)) { + return; + } else if (target.indexOf("sleep") > 0) { + sleepUninterruptibly(100, TimeUnit.MILLISECONDS); + response.setContentType("text/xml"); + response.setStatus(SC_OK); + } else if (target.indexOf("redirect") > 0) { + // in OpenJDK 7.0, expect continue handling is enforced, so we + // have to consume the stream. + // http://hg.openjdk.java.net/jdk7/tl/jdk/rev/045aeb76b0ff + // getInputStream address the expect-continue, per jetty docs + // http://wiki.eclipse.org/Jetty/Feature/1xx_Responses#100_Continue + toStringAndClose(request.getInputStream()); + response.sendRedirect("https://localhost:" + (testPort + 1) + "/"); + } else if (target.indexOf("101constitutions") > 0) { + response.setContentType("text/plain"); + response.setHeader("Content-MD5", md5); + response.setStatus(SC_OK); + copy(oneHundredOneConstitutions, response.getOutputStream()); + } else if (request.getMethod().equals("PUT")) { + if (request.getContentLength() > 0) { + response.setStatus(SC_OK); + response.getWriter().println(toStringAndClose(request.getInputStream()) + "PUT"); } else { - if (failEveryTenRequests(request, response)) - return; - response.setContentType("text/xml"); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(XML); + response.setStatus(SC_OK); } - ((Request) request).setHandled(true); - } catch (IOException e) { - if (body != null) - closeQuietly(body); - response.sendError(500, Throwables.getStackTraceAsString(e)); + } else if (request.getMethod().equals("POST")) { + // don't redirect large objects + if (request.getContentLength() < 10240 && redirectEveryTwentyRequests(request, response)) + return; + if (failEveryTenRequests(request, response)) + return; + if (request.getContentLength() > 0) { + handlePost(request, response); + } else { + handleAction(request, response); + } + } else if (request.getHeader("range") != null) { + response.sendError(404, "no content"); + } else if (request.getHeader("test") != null) { + response.setContentType("text/plain"); + response.setStatus(SC_OK); + response.getWriter().println("test"); + } else if (request.getMethod().equals("HEAD")) { + // by HTML specification, HEAD response MUST NOT include a body + response.setContentType("text/xml"); + response.setStatus(SC_OK); + } else { + if (failEveryTenRequests(request, response)) + return; + response.setContentType("text/xml"); + response.setStatus(SC_OK); + response.getWriter().println(XML); } + Request.class.cast(request).setHandled(true); } }; @@ -196,65 +187,54 @@ public abstract class BaseJettyTest { realMd5FromRequest = CryptoStreams.md5Base64(InputSuppliers.of(body)); boolean matched = expectedMd5.equals(realMd5FromRequest); if (matched) { - response.setStatus(HttpServletResponse.SC_OK); + response.setStatus(SC_OK); response.addHeader("x-Content-MD5", realMd5FromRequest); } else { response.sendError(500, "didn't match"); } } else { - String responseString = (request.getContentLength() < 10240) ? Strings2.toStringAndClose(body) + "POST" - : "POST"; + String responseString = (request.getContentLength() < 10240) ? toStringAndClose(body) + "POST" : "POST"; body = null; for (String header : new String[] { CONTENT_DISPOSITION, CONTENT_LANGUAGE, CONTENT_ENCODING }) if (request.getHeader(header) != null) { response.addHeader("x-" + header, request.getHeader(header)); } - response.setStatus(HttpServletResponse.SC_OK); + response.setStatus(SC_OK); response.getWriter().println(responseString); } + Request.class.cast(request).setHandled(true); } catch (IOException e) { - if (body != null) - closeQuietly(body); - response.sendError(500, Throwables.getStackTraceAsString(e)); + closeQuietly(body); + response.sendError(500, getStackTraceAsString(e)); } } protected void setupAndStartSSLServer(final int testPort) throws Exception { Handler server2Handler = new AbstractHandler() { public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) - throws IOException, ServletException { - InputStream body = request.getInputStream(); - try { - if (request.getMethod().equals("PUT")) { - String text = Strings2.toStringAndClose(body); - body = null; - if (request.getContentLength() > 0) { - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(text + "PUTREDIRECT"); - } - } else if (request.getMethod().equals("POST")) { - if (request.getContentLength() > 0) { - handlePost(request, response); - } else { - handleAction(request, response); - } - } else if (request.getMethod().equals("HEAD")) { - /* - * NOTE: by HTML specification, HEAD response MUST NOT include a body - */ - response.setContentType("text/xml"); - response.setStatus(HttpServletResponse.SC_OK); - } else { - response.setContentType("text/xml"); - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(XML2); + throws IOException, ServletException { + if (request.getMethod().equals("PUT")) { + if (request.getContentLength() > 0) { + response.setStatus(SC_OK); + String text = toStringAndClose(request.getInputStream()); + response.getWriter().println(text + "PUTREDIRECT"); } - ((Request) request).setHandled(true); - } catch (IOException e) { - if (body != null) - closeQuietly(body); - response.sendError(500, Throwables.getStackTraceAsString(e)); + } else if (request.getMethod().equals("POST")) { + if (request.getContentLength() > 0) { + handlePost(request, response); + } else { + handleAction(request, response); + } + } else if (request.getMethod().equals("HEAD")) { + // by HTML specification, HEAD response MUST NOT include a body + response.setContentType("text/xml"); + response.setStatus(SC_OK); + } else { + response.setContentType("text/xml"); + response.setStatus(SC_OK); + response.getWriter().println(XML2); } + Request.class.cast(request).setHandled(true); } }; @@ -270,8 +250,8 @@ public abstract class BaseJettyTest { ssl.setTrustStore("src/test/resources/test.jks"); ssl.setTrustStorePassword("jclouds"); - server2.setConnectors(new Connector[]{ ssl_connector }); - + server2.setConnectors(new Connector[] { ssl_connector }); + server2.start(); } @@ -289,18 +269,18 @@ public abstract class BaseJettyTest { } public static ContextBuilder newBuilder(int testPort, Properties properties, Module... connectionModules) { - properties.setProperty(Constants.PROPERTY_TRUST_ALL_CERTS, "true"); - properties.setProperty(Constants.PROPERTY_RELAX_HOSTNAME, "true"); - return ContextBuilder.newBuilder( - AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class, IntegrationTestAsyncClient.class, - "http://localhost:" + testPort)) - .modules(ImmutableSet. copyOf(connectionModules)) - .overrides(properties); + properties.setProperty(PROPERTY_TRUST_ALL_CERTS, "true"); + properties.setProperty(PROPERTY_RELAX_HOSTNAME, "true"); + return ContextBuilder + .newBuilder( + AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class, + IntegrationTestAsyncClient.class, "http://localhost:" + testPort)) + .modules(ImmutableSet. copyOf(connectionModules)).overrides(properties); } - @AfterTest + @AfterClass public void tearDownJetty() throws Exception { - context.close(); + closeQuietly(context); if (server2 != null) server2.stop(); server.stop(); @@ -321,17 +301,17 @@ public abstract class BaseJettyTest { protected boolean failEveryTenRequests(HttpServletRequest request, HttpServletResponse response) throws IOException { if (cycle.incrementAndGet() % 10 == 0) { response.sendError(500, "unlucky 10"); - ((Request) request).setHandled(true); + Request.class.cast(request).setHandled(true); return true; } return false; } protected boolean redirectEveryTwentyRequests(HttpServletRequest request, HttpServletResponse response) - throws IOException { + throws IOException { if (cycle.incrementAndGet() % 20 == 0) { response.sendRedirect("http://localhost:" + (testPort + 1) + "/"); - ((Request) request).setHandled(true); + Request.class.cast(request).setHandled(true); return true; } return false; @@ -351,7 +331,7 @@ public abstract class BaseJettyTest { response.getWriter().println("no content length!"); response.getWriter().println(realHeaders.toString()); response.sendError(500, "no content length!"); - ((Request) request).setHandled(true); + Request.class.cast(request).setHandled(true); return true; } return false; @@ -363,20 +343,15 @@ public abstract class BaseJettyTest { if (matchFound) { String objectId = matcher.group(1); String action = matcher.group(2); - Map options = newHashMap(); + Builder options = ImmutableMap. builder(); 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]); - } - } + options.putAll(Splitter.on(';').withKeyValueSeparator("=").split(matcher.group(3))); } - response.setStatus(HttpServletResponse.SC_OK); - response.getWriter().println(objectId + "->" + action + ":" + options); + response.setStatus(SC_OK); + response.getWriter().println(objectId + "->" + action + ":" + options.build()); } else { response.sendError(500, "no content"); } } + } diff --git a/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java b/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java index 13efc4aa83..b8642f02b1 100644 --- a/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java +++ b/core/src/test/java/org/jclouds/http/IntegrationTestAsyncClient.java @@ -56,6 +56,7 @@ import com.google.inject.Provides; /** * Sample test for the behaviour of our Integration Test jetty server. * + * @see IntegrationTestClient * @author Adrian Cole */ public interface IntegrationTestAsyncClient { @@ -192,6 +193,10 @@ public interface IntegrationTestAsyncClient { } + @PUT + @Path("/objects/{id}") + ListenableFuture putNothing(@PathParam("id") String id); + @Provides StringBuilder newStringBuilder(); diff --git a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java index 3d60a2b190..ea61d23977 100644 --- a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java +++ b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java @@ -64,6 +64,8 @@ public interface IntegrationTestClient { String downloadAndParse(String id); + void putNothing(String id); + @Provides StringBuilder newStringBuilder(); }