diff --git a/core/src/main/java/org/jclouds/Constants.java b/core/src/main/java/org/jclouds/Constants.java index a658026cf7..115edf1180 100644 --- a/core/src/main/java/org/jclouds/Constants.java +++ b/core/src/main/java/org/jclouds/Constants.java @@ -307,6 +307,12 @@ public final class Constants { */ public static final String PROPERTY_STRIP_EXPECT_HEADER = "jclouds.strip-expect-header"; + /** + * When true, add the Connection: close header. Useful when interacting with + * providers that don't properly support persistent connections. Defaults to false. + */ + public static final String PROPERTY_CONNECTION_CLOSE_HEADER = "jclouds.connection-close-header"; + /** * The maximum number of blob deletes happening in parallel at any point in time. */ diff --git a/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java b/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java index 7b5ff35935..1cfbb6fd2b 100644 --- a/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java +++ b/core/src/main/java/org/jclouds/apis/internal/BaseApiMetadata.java @@ -18,6 +18,7 @@ package org.jclouds.apis.internal; import static com.google.common.base.Objects.equal; import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.Constants.PROPERTY_CONNECTION_CLOSE_HEADER; import static org.jclouds.Constants.PROPERTY_CONNECTION_TIMEOUT; import static org.jclouds.Constants.PROPERTY_ISO3166_CODES; import static org.jclouds.Constants.PROPERTY_MAX_CONNECTIONS_PER_CONTEXT; @@ -73,6 +74,7 @@ public abstract class BaseApiMetadata implements ApiMetadata { props.setProperty(PROPERTY_SESSION_INTERVAL, 60 + ""); props.setProperty(PROPERTY_PRETTY_PRINT_PAYLOADS, "true"); props.setProperty(PROPERTY_STRIP_EXPECT_HEADER, "false"); + props.setProperty(PROPERTY_CONNECTION_CLOSE_HEADER, "false"); // By default, we allow maximum parallel deletes to be equal to the number // of user threads since one thread is used to delete on blob. diff --git a/core/src/main/java/org/jclouds/http/filters/ConnectionCloseHeader.java b/core/src/main/java/org/jclouds/http/filters/ConnectionCloseHeader.java new file mode 100644 index 0000000000..ba2b55c56c --- /dev/null +++ b/core/src/main/java/org/jclouds/http/filters/ConnectionCloseHeader.java @@ -0,0 +1,33 @@ +/* + * 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.http.filters; + +import org.jclouds.http.HttpException; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpRequestFilter; + +import com.google.common.net.HttpHeaders; +import com.google.inject.Singleton; + +@Singleton +public class ConnectionCloseHeader implements HttpRequestFilter { + @Override + public HttpRequest filter(HttpRequest request) throws HttpException { + return request.toBuilder().addHeader(HttpHeaders.CONNECTION, "close").build(); + } +} diff --git a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java index 74b1ac17c4..f6ebc8cbf8 100644 --- a/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java +++ b/core/src/main/java/org/jclouds/rest/internal/RestAnnotationProcessor.java @@ -51,6 +51,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; + import javax.annotation.Resource; import javax.inject.Named; import javax.ws.rs.FormParam; @@ -65,6 +66,7 @@ import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpUtils; import org.jclouds.http.Uris.UriBuilder; +import org.jclouds.http.filters.ConnectionCloseHeader; import org.jclouds.http.filters.StripExpectHeader; import org.jclouds.http.options.HttpRequestOptions; import org.jclouds.io.ContentMetadataCodec; @@ -147,12 +149,14 @@ public class RestAnnotationProcessor implements Function tokenValues = LinkedHashMultimap.create(); diff --git a/core/src/test/java/org/jclouds/http/filters/ConnectionCloseHeaderTest.java b/core/src/test/java/org/jclouds/http/filters/ConnectionCloseHeaderTest.java new file mode 100644 index 0000000000..342b14c621 --- /dev/null +++ b/core/src/test/java/org/jclouds/http/filters/ConnectionCloseHeaderTest.java @@ -0,0 +1,38 @@ +/* + * 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.http.filters; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import org.jclouds.http.HttpRequest; +import org.testng.annotations.Test; + +import com.google.common.net.HttpHeaders; + +@Test(groups = "unit") +public class ConnectionCloseHeaderTest { + public void testConnectionHeaderIsAdded() { + HttpRequest request = HttpRequest.builder().method("POST").endpoint("http://localhost").build(); + request = new ConnectionCloseHeader().filter(request); + + assertTrue(request.getHeaders().containsKey(HttpHeaders.CONNECTION)); + assertEquals(request.getHeaders().get(HttpHeaders.CONNECTION).size(), 1); + assertEquals(request.getHeaders().get(HttpHeaders.CONNECTION).iterator().next(), "close"); + } +} 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 40e8f83b56..7e435513cd 100644 --- a/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/RestAnnotationProcessorTest.java @@ -78,6 +78,7 @@ import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpRequestFilter; import org.jclouds.http.HttpResponse; import org.jclouds.http.IOExceptionRetryHandler; +import org.jclouds.http.filters.ConnectionCloseHeader; import org.jclouds.http.filters.StripExpectHeader; import org.jclouds.http.functions.ParseFirstJsonValueNamed; import org.jclouds.http.functions.ParseJson; @@ -1440,6 +1441,34 @@ public class RestAnnotationProcessorTest extends BaseRestApiTest { assertEquals(request.getFilters().get(1).getClass(), StripExpectHeader.class); } + @Test + public void testRequestFilterAddConnection() { + // First, verify that by default, the StripExpectHeader filter is not applied + Invokable method = method(TestRequestFilter.class, "post"); + Invocation invocation = Invocation.create(method, + ImmutableList.of(HttpRequest.builder().method("POST").endpoint("http://localhost").build())); + GeneratedHttpRequest request = processor.apply(invocation); + assertEquals(request.getFilters().size(), 1); + assertEquals(request.getFilters().get(0).getClass(), TestRequestFilter1.class); + + // Now let's create a new injector with the property set. Use that to create the annotation processor. + Properties overrides = new Properties(); + overrides.setProperty(Constants.PROPERTY_CONNECTION_CLOSE_HEADER, "true"); + Injector injector = ContextBuilder.newBuilder(forApiOnEndpoint(Callee.class, "http://localhost:9999")) + .modules(ImmutableSet. of(new MockModule(), new NullLoggingModule(), new AbstractModule() { + protected void configure() { + bind(new TypeLiteral>() { + }).annotatedWith(Localhost2.class).toInstance( + Suppliers.ofInstance(URI.create("http://localhost:1111"))); + }})) + .overrides(overrides).buildInjector(); + RestAnnotationProcessor newProcessor = injector.getInstance(RestAnnotationProcessor.class); + // Verify that this time the filter is indeed applied as expected. + request = newProcessor.apply(invocation); + assertEquals(request.getFilters().size(), 2); + assertEquals(request.getFilters().get(1).getClass(), ConnectionCloseHeader.class); + } + public class TestEncoding { @GET @Path("/{path1}/{path2}")