diff --git a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/config/CloudStackRestClientModule.java b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/config/CloudStackRestClientModule.java index 3ec22b87ea..fcc9422754 100644 --- a/apis/cloudstack/src/main/java/org/jclouds/cloudstack/config/CloudStackRestClientModule.java +++ b/apis/cloudstack/src/main/java/org/jclouds/cloudstack/config/CloudStackRestClientModule.java @@ -18,7 +18,7 @@ */ package org.jclouds.cloudstack.config; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -207,9 +207,9 @@ public class CloudStackRestClientModule extends RestClientModule extends RestClientModule { diff --git a/apis/swift/src/main/java/org/jclouds/openstack/swift/blobstore/config/TemporaryUrlExtensionModule.java b/apis/swift/src/main/java/org/jclouds/openstack/swift/blobstore/config/TemporaryUrlExtensionModule.java index bcc6882ed5..d322c7d943 100644 --- a/apis/swift/src/main/java/org/jclouds/openstack/swift/blobstore/config/TemporaryUrlExtensionModule.java +++ b/apis/swift/src/main/java/org/jclouds/openstack/swift/blobstore/config/TemporaryUrlExtensionModule.java @@ -18,7 +18,7 @@ */ package org.jclouds.openstack.swift.blobstore.config; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; import java.util.concurrent.TimeUnit; @@ -70,7 +70,7 @@ public abstract class TemporaryUrlExtensionModule { protected void bindTemporaryUrlKeyApi() { - bindHttpApi(binder(), TemporaryUrlKeyApi.class, KeystoneTemporaryUrlKeyAsyncApi.class); + bindMappedHttpApi(binder(), TemporaryUrlKeyApi.class, KeystoneTemporaryUrlKeyAsyncApi.class); } @Override @@ -115,7 +115,7 @@ public abstract class TemporaryUrlExtensionModule>() { }).to(new TypeLiteral() { }); - bindHttpApi(binder(), VCloudVersionsClient.class, VCloudVersionsAsyncClient.class); - bindHttpApi(binder(), VCloudLoginClient.class, VCloudLoginAsyncClient.class); + bindMappedHttpApi(binder(), VCloudVersionsClient.class, VCloudVersionsAsyncClient.class); + bindMappedHttpApi(binder(), VCloudLoginClient.class, VCloudLoginAsyncClient.class); } protected void bindCacheLoaders() { diff --git a/blobstore/src/main/java/org/jclouds/blobstore/config/TransientBlobStoreContextModule.java b/blobstore/src/main/java/org/jclouds/blobstore/config/TransientBlobStoreContextModule.java index e04e90ceec..a7813b2cbc 100644 --- a/blobstore/src/main/java/org/jclouds/blobstore/config/TransientBlobStoreContextModule.java +++ b/blobstore/src/main/java/org/jclouds/blobstore/config/TransientBlobStoreContextModule.java @@ -18,7 +18,7 @@ */ package org.jclouds.blobstore.config; -import static org.jclouds.rest.config.BinderUtils.bindBlockingApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedApi; import org.jclouds.blobstore.AsyncBlobStore; import org.jclouds.blobstore.BlobRequestSigner; @@ -41,7 +41,7 @@ public class TransientBlobStoreContextModule extends AbstractModule { protected void configure() { bind(AsyncBlobStore.class).to(LocalAsyncBlobStore.class).asEagerSingleton(); // forward all requests from TransientBlobStore to TransientAsyncBlobStore. needs above binding as cannot proxy a class - bindBlockingApi(binder(), LocalBlobStore.class, AsyncBlobStore.class); + bindMappedApi(binder(), LocalBlobStore.class, AsyncBlobStore.class); install(new BlobStoreObjectModule()); install(new BlobStoreMapModule()); bind(BlobStore.class).to(LocalBlobStore.class); diff --git a/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java b/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java index 6fdacfd6a7..328d5876d6 100644 --- a/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java +++ b/common/openstack/src/main/java/org/jclouds/openstack/config/OpenStackAuthenticationModule.java @@ -19,7 +19,7 @@ package org.jclouds.openstack.config; import static com.google.common.base.Suppliers.memoizeWithExpiration; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; import java.util.Date; import java.util.concurrent.ExecutionException; @@ -59,7 +59,7 @@ public class OpenStackAuthenticationModule extends AbstractModule { @Override protected void configure() { // OpenStackAuthClient is used directly for filters and retry handlers, so let's bind it explicitly - bindHttpApi(binder(), OpenStackAuthClient.class, OpenStackAuthAsyncClient.class); + bindMappedHttpApi(binder(), OpenStackAuthClient.class, OpenStackAuthAsyncClient.class); install(new FactoryModuleBuilder().build(URIFromAuthenticationResponseForService.Factory.class)); bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(RetryOnRenew.class); } diff --git a/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/config/AuthenticationServiceModule.java b/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/config/AuthenticationServiceModule.java index a7fabd78c2..167ab3ce5e 100644 --- a/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/config/AuthenticationServiceModule.java +++ b/common/openstack/src/main/java/org/jclouds/openstack/keystone/v1_1/config/AuthenticationServiceModule.java @@ -18,7 +18,7 @@ */ package org.jclouds.openstack.keystone.v1_1.config; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; import java.util.concurrent.ExecutionException; @@ -60,7 +60,7 @@ public class AuthenticationServiceModule extends AbstractModule { @Override protected void configure() { // ServiceClient is used directly for filters and retry handlers, so let's bind it explicitly - bindHttpApi(binder(), AuthenticationClient.class, AuthenticationAsyncClient.class); + bindMappedHttpApi(binder(), AuthenticationClient.class, AuthenticationAsyncClient.class); install(new FactoryModuleBuilder().implement(RegionIdToURISupplier.class, RegionIdToURIFromAuthForServiceSupplier.class).build(RegionIdToURISupplier.Factory.class)); install(new FactoryModuleBuilder().implement(ImplicitRegionIdSupplier.class, V1DefaultRegionIdSupplier.class) diff --git a/common/trmk/src/main/java/org/jclouds/trmk/vcloud_0_8/config/TerremarkVCloudRestClientModule.java b/common/trmk/src/main/java/org/jclouds/trmk/vcloud_0_8/config/TerremarkVCloudRestClientModule.java index 2042f71231..463767aef4 100644 --- a/common/trmk/src/main/java/org/jclouds/trmk/vcloud_0_8/config/TerremarkVCloudRestClientModule.java +++ b/common/trmk/src/main/java/org/jclouds/trmk/vcloud_0_8/config/TerremarkVCloudRestClientModule.java @@ -26,7 +26,7 @@ import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Maps.transformValues; import static com.google.common.collect.Maps.uniqueIndex; import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; import static org.jclouds.trmk.vcloud_0_8.reference.VCloudConstants.PROPERTY_VCLOUD_TIMEOUT_TASK_COMPLETED; import static org.jclouds.util.Predicates2.retry; @@ -134,8 +134,8 @@ public class TerremarkVCloudRestClientModule extends RestClientModule>>() { }).to(new TypeLiteral() { }); - bindHttpApi(binder(), TerremarkVCloudVersionsClient.class, TerremarkVCloudVersionsAsyncClient.class); - bindHttpApi(binder(), TerremarkVCloudLoginClient.class, TerremarkVCloudLoginAsyncClient.class); + bindMappedHttpApi(binder(), TerremarkVCloudVersionsClient.class, TerremarkVCloudVersionsAsyncClient.class); + bindMappedHttpApi(binder(), TerremarkVCloudLoginClient.class, TerremarkVCloudLoginAsyncClient.class); } @Provides diff --git a/compute/src/main/java/org/jclouds/compute/internal/UtilsImpl.java b/compute/src/main/java/org/jclouds/compute/internal/UtilsImpl.java index 9e1f00b977..1f6b9661ac 100644 --- a/compute/src/main/java/org/jclouds/compute/internal/UtilsImpl.java +++ b/compute/src/main/java/org/jclouds/compute/internal/UtilsImpl.java @@ -31,7 +31,6 @@ import org.jclouds.date.DateService; import org.jclouds.domain.Credentials; import org.jclouds.json.Json; import org.jclouds.logging.Logger.LoggerFactory; -import org.jclouds.rest.HttpAsyncClient; import org.jclouds.rest.HttpClient; import org.jclouds.ssh.SshClient; import org.jclouds.ssh.SshClient.Factory; @@ -54,11 +53,12 @@ public class UtilsImpl extends org.jclouds.rest.internal.UtilsImpl implements Ut private final Function sshForNode; @Inject - UtilsImpl(Injector injector, Json json, XMLParser xml, HttpClient simpleClient, HttpAsyncClient simpleAsyncClient, - Crypto encryption, DateService date, @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, - @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor, EventBus eventBus, - Map credentialStore, LoggerFactory loggerFactory, - Function sshForNode) { + UtilsImpl(Injector injector, Json json, XMLParser xml, HttpClient simpleClient, + org.jclouds.rest.HttpAsyncClient simpleAsyncClient, Crypto encryption, DateService date, + @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, + @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor, EventBus eventBus, + Map credentialStore, LoggerFactory loggerFactory, + Function sshForNode) { super(injector, json, xml, simpleClient, simpleAsyncClient, encryption, date, userExecutor, ioExecutor, eventBus, credentialStore, loggerFactory); this.sshForNode = sshForNode; diff --git a/core/src/main/java/org/jclouds/ContextBuilder.java b/core/src/main/java/org/jclouds/ContextBuilder.java index ccdf4eb296..2a1b6cc8ca 100644 --- a/core/src/main/java/org/jclouds/ContextBuilder.java +++ b/core/src/main/java/org/jclouds/ContextBuilder.java @@ -20,10 +20,12 @@ package org.jclouds; import static com.google.common.base.Objects.toStringHelper; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.containsPattern; import static com.google.common.base.Predicates.instanceOf; import static com.google.common.base.Predicates.not; import static com.google.common.base.Predicates.notNull; +import static com.google.common.base.Predicates.or; import static com.google.common.base.Throwables.propagate; import static com.google.common.collect.Iterables.addAll; import static com.google.common.collect.Iterables.any; @@ -57,6 +59,7 @@ import org.jclouds.apis.Apis; import org.jclouds.concurrent.SingleThreaded; import org.jclouds.concurrent.config.ConfiguresExecutorService; import org.jclouds.concurrent.config.ExecutorServiceModule; +import org.jclouds.config.BindApiContextWithWildcardExtendsExplicitAndRawType; import org.jclouds.config.BindNameToContext; import org.jclouds.config.BindPropertiesToExpandedValues; import org.jclouds.config.BindRestContextWithWildcardExtendsExplicitAndRawType; @@ -75,9 +78,14 @@ import org.jclouds.providers.Providers; import org.jclouds.providers.config.BindProviderMetadataContextAndCredentials; import org.jclouds.providers.internal.UpdateProviderMetadataFromProperties; import org.jclouds.rest.ConfiguresCredentialStore; +import org.jclouds.rest.ConfiguresHttpApi; import org.jclouds.rest.ConfiguresRestClient; +import org.jclouds.rest.HttpApiMetadata; import org.jclouds.rest.RestApiMetadata; +import org.jclouds.rest.RestContext; import org.jclouds.rest.config.CredentialStoreModule; +import org.jclouds.rest.config.HttpApiModule; +import org.jclouds.rest.config.MappedHttpInvocationModule; import org.jclouds.rest.config.RestClientModule; import org.jclouds.rest.config.RestModule; @@ -382,9 +390,9 @@ public class ContextBuilder { public static Injector buildInjector(String name, ProviderMetadata providerMetadata, Supplier creds, List inputModules) { List modules = newArrayList(); modules.addAll(inputModules); - boolean restModuleSpecifiedByUser = restClientModulePresent(inputModules); - Iterable defaultModules = ifSpecifiedByUserDontIncludeDefaultRestModule( - providerMetadata.getApiMetadata(), restModuleSpecifiedByUser); + boolean apiModuleSpecifiedByUser = apiModulePresent(inputModules); + Iterable defaultModules = ifSpecifiedByUserDontIncludeDefaultApiModule( + providerMetadata.getApiMetadata(), apiModuleSpecifiedByUser); addAll(modules, defaultModules); addClientModuleIfNotPresent(providerMetadata.getApiMetadata(), modules); addRestContextBinding(providerMetadata.getApiMetadata(), modules); @@ -415,17 +423,24 @@ public class ContextBuilder { } static void addRestContextBinding(ApiMetadata apiMetadata, List modules) { - if (apiMetadata instanceof RestApiMetadata) { + if (apiMetadata instanceof HttpApiMetadata) { + try { + modules + .add(new BindApiContextWithWildcardExtendsExplicitAndRawType(HttpApiMetadata.class.cast(apiMetadata))); + } catch (IllegalArgumentException e) { + + } + } else if (apiMetadata instanceof RestApiMetadata) { try { modules.add(new BindRestContextWithWildcardExtendsExplicitAndRawType(RestApiMetadata.class - .cast(apiMetadata))); + .cast(apiMetadata))); } catch (IllegalArgumentException e) { } } } - static Iterable ifSpecifiedByUserDontIncludeDefaultRestModule(ApiMetadata apiMetadata, + static Iterable ifSpecifiedByUserDontIncludeDefaultApiModule(ApiMetadata apiMetadata, boolean restModuleSpecifiedByUser) { Iterable defaultModules = transform(apiMetadata.getDefaultModules(), new Function, Module>() { @@ -443,7 +458,7 @@ public class ContextBuilder { }); if (restModuleSpecifiedByUser) - defaultModules = filter(defaultModules, not(configuresRest)); + defaultModules = filter(defaultModules, and(not(configuresApi), not(configuresRest))); return defaultModules; } @@ -476,30 +491,40 @@ public class ContextBuilder { @VisibleForTesting static void addClientModuleIfNotPresent(ApiMetadata apiMetadata, List modules) { - if (!restClientModulePresent(modules)) { + if (!apiModulePresent(modules)) { addClientModule(apiMetadata, modules); } } + private static boolean apiModulePresent(List modules) { + return any(modules, or(configuresApi, configuresRest)); + } - static Predicate configuresRest = new Predicate() { + private static Predicate configuresApi = new Predicate() { + public boolean apply(Module input) { + return input.getClass().isAnnotationPresent(ConfiguresHttpApi.class); + } + + }; + + private static Predicate configuresRest = new Predicate() { public boolean apply(Module input) { return input.getClass().isAnnotationPresent(ConfiguresRestClient.class); } }; - static boolean restClientModulePresent(List modules) { - return any(modules, configuresRest); - } - @SuppressWarnings({ "unchecked", "rawtypes" }) static void addClientModule(ApiMetadata apiMetadata, List modules) { // TODO: move this up - if (apiMetadata instanceof RestApiMetadata) { + if (apiMetadata instanceof HttpApiMetadata) { + HttpApiMetadata api = HttpApiMetadata.class.cast(apiMetadata); + modules.add(new HttpApiModule(api.getApi())); + } else if (apiMetadata instanceof RestApiMetadata) { RestApiMetadata rest = RestApiMetadata.class.cast(apiMetadata); modules.add(new RestClientModule(typeToken(rest.getApi()), typeToken(rest.getAsyncApi()))); } else { modules.add(new RestModule()); + modules.add(new MappedHttpInvocationModule()); } } diff --git a/core/src/main/java/org/jclouds/config/BindApiContextWithWildcardExtendsExplicitAndRawType.java b/core/src/main/java/org/jclouds/config/BindApiContextWithWildcardExtendsExplicitAndRawType.java new file mode 100644 index 0000000000..b4f641c560 --- /dev/null +++ b/core/src/main/java/org/jclouds/config/BindApiContextWithWildcardExtendsExplicitAndRawType.java @@ -0,0 +1,70 @@ +/** + * 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.config; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.reflect.Reflection2.typeToken; + +import org.jclouds.rest.ApiContext; +import org.jclouds.rest.HttpApiMetadata; +import org.jclouds.rest.internal.ApiContextImpl; +import org.jclouds.rest.internal.BaseHttpApiMetadata; + +import com.google.common.reflect.TypeToken; +import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; +import com.google.inject.util.Types; + +/** + * Allows you to lookup the {@link HttpApiMetadata#getContext()} as + * {@link ApiContext}, {@code ApiContext}, and {@code ApiContext}. + * + * @author Adrian Cole + */ +public class BindApiContextWithWildcardExtendsExplicitAndRawType extends AbstractModule { + private final HttpApiMetadata httpApiMetadata; + + public BindApiContextWithWildcardExtendsExplicitAndRawType(HttpApiMetadata httpApiMetadata) + throws IllegalArgumentException { + this.httpApiMetadata = checkNotNull(httpApiMetadata, "httpApiMetadata"); + checkArgument(httpApiMetadata.getContext().getRawType().equals(ApiContext.class), + "this does not work as %s raw type is not ApiContext", httpApiMetadata.getContext()); + } + + @SuppressWarnings("unchecked") + @Override + protected void configure() { + TypeToken concreteType = BaseHttpApiMetadata.contextToken(typeToken(httpApiMetadata.getApi())); + // bind explicit type + bind(TypeLiteral.get(concreteType.getType())).to( + TypeLiteral.class.cast(TypeLiteral.get(Types.newParameterizedType(ApiContextImpl.class, + httpApiMetadata.getApi())))); + // bind potentially wildcard type + if (!concreteType.equals(httpApiMetadata.getContext())) { + bind(TypeLiteral.get(httpApiMetadata.getContext().getType())).to( + TypeLiteral.class.cast(TypeLiteral.get(Types.newParameterizedType(ApiContextImpl.class, + httpApiMetadata.getApi())))); + } + // bind w/o types + bind(TypeLiteral.get(ApiContext.class)).to( + TypeLiteral.class.cast(TypeLiteral.get(Types.newParameterizedType(ApiContextImpl.class, + httpApiMetadata.getApi())))); + } +} diff --git a/core/src/main/java/org/jclouds/config/BindRestContextWithWildcardExtendsExplicitAndRawType.java b/core/src/main/java/org/jclouds/config/BindRestContextWithWildcardExtendsExplicitAndRawType.java index d14c554b74..e47d718e74 100644 --- a/core/src/main/java/org/jclouds/config/BindRestContextWithWildcardExtendsExplicitAndRawType.java +++ b/core/src/main/java/org/jclouds/config/BindRestContextWithWildcardExtendsExplicitAndRawType.java @@ -35,7 +35,10 @@ import com.google.inject.util.Types; * Allows you to lookup the {@link RestApiMetadata#getContext()} as {@link RestContext}, {@code RestContext}, and {@code * * @author Adrian Cole + * @deprecated please use {@link BindApiContextWithWildcardExtendsExplicitAndRawType} as + * async interface will be removed in jclouds 1.7. */ +@Deprecated public class BindRestContextWithWildcardExtendsExplicitAndRawType extends AbstractModule { private final RestApiMetadata restApiMetadata; diff --git a/core/src/main/java/org/jclouds/providers/AnonymousProviderMetadata.java b/core/src/main/java/org/jclouds/providers/AnonymousProviderMetadata.java index 5bbdeeedd7..ccede4c6d4 100644 --- a/core/src/main/java/org/jclouds/providers/AnonymousProviderMetadata.java +++ b/core/src/main/java/org/jclouds/providers/AnonymousProviderMetadata.java @@ -22,6 +22,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import org.jclouds.apis.ApiMetadata; import org.jclouds.providers.internal.BaseProviderMetadata; +import org.jclouds.rest.AnonymousHttpApiMetadata; import org.jclouds.rest.AnonymousRestApiMetadata; /** @@ -31,11 +32,14 @@ import org.jclouds.rest.AnonymousRestApiMetadata; */ public class AnonymousProviderMetadata extends BaseProviderMetadata { + public static ProviderMetadata forApiOnEndpoint(Class api, String endpoint) { + return forApiWithEndpoint(AnonymousHttpApiMetadata.forApi(api), endpoint); + } + public static ProviderMetadata forClientMappedToAsyncClientOnEndpoint(Class client, Class asyncClient, String endpoint) { return forApiWithEndpoint(AnonymousRestApiMetadata.forClientMappedToAsyncClient(client, asyncClient), endpoint); } - public static ProviderMetadata forApiWithEndpoint(ApiMetadata md, String endpoint) { checkNotNull(md, "api"); checkNotNull(endpoint, "endpoint (%s)", md.getEndpointName()); diff --git a/core/src/main/java/org/jclouds/reflect/Types2.java b/core/src/main/java/org/jclouds/reflect/Types2.java new file mode 100644 index 0000000000..c900d3dee7 --- /dev/null +++ b/core/src/main/java/org/jclouds/reflect/Types2.java @@ -0,0 +1,45 @@ +/** + * 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.reflect; + +import static com.google.common.base.Preconditions.checkState; + +import java.lang.reflect.TypeVariable; + +import com.google.common.annotations.Beta; +import com.google.common.reflect.TypeToken; + +/** + * @since 1.7 + */ +@Beta +public class Types2 { + + /** + * Helpful when you are capturing the type inside a constructor. + * + * @throws IllegalStateException + * if the type is an instanceof {@link TypeVariable} + */ + public static TypeToken checkBound(TypeToken type) throws IllegalStateException { + checkState(!(type.getType() instanceof TypeVariable), + "unbound type variable: %s, use ctor that explicitly assigns this", type); + return type; + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/AnonymousHttpApiMetadata.java b/core/src/main/java/org/jclouds/rest/AnonymousHttpApiMetadata.java new file mode 100644 index 0000000000..b5335ca0f0 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/AnonymousHttpApiMetadata.java @@ -0,0 +1,69 @@ +/** + * 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.rest; + +import java.net.URI; + +import org.jclouds.rest.internal.BaseHttpApiMetadata; + +import com.google.common.annotations.Beta; + +/** + * Useful in creating arbitrary http apis. + * + * @author Adrian Cole + */ +@Beta +public class AnonymousHttpApiMetadata extends BaseHttpApiMetadata { + + public static AnonymousHttpApiMetadata forApi(Class httpApi) { + return new Builder(httpApi).build(); + } + + @Override + public Builder toBuilder() { + return new Builder(getApi()).fromApiMetadata(this); + } + + private AnonymousHttpApiMetadata(Builder builder) { + super(builder); + } + + private static final class Builder extends BaseHttpApiMetadata.Builder> { + + private Builder(Class api) { + super(api); + id(api.getSimpleName()) + .identityName("unused") + .defaultIdentity("foo") + .version("1") + .documentation(URI.create("http://jclouds.org/documentation")); + } + + @Override + public AnonymousHttpApiMetadata build() { + return new AnonymousHttpApiMetadata(this); + } + + @Override + protected Builder self() { + return this; + } + } +} diff --git a/core/src/main/java/org/jclouds/rest/AnonymousRestApiMetadata.java b/core/src/main/java/org/jclouds/rest/AnonymousRestApiMetadata.java index 8eecd360de..b5d10b41b8 100644 --- a/core/src/main/java/org/jclouds/rest/AnonymousRestApiMetadata.java +++ b/core/src/main/java/org/jclouds/rest/AnonymousRestApiMetadata.java @@ -20,6 +20,7 @@ package org.jclouds.rest; import java.net.URI; +import org.jclouds.rest.internal.BaseHttpApiMetadata; import org.jclouds.rest.internal.BaseRestApiMetadata; import com.google.common.annotations.Beta; @@ -28,6 +29,8 @@ import com.google.common.annotations.Beta; * Useful in creating arbitrary clients. * * @author Adrian Cole + * @deprecated please use {@link AnonymousHttpApiMetadata} as + * async interface will be removed in jclouds 1.7. */ @Beta public class AnonymousRestApiMetadata extends BaseRestApiMetadata { diff --git a/core/src/main/java/org/jclouds/rest/ApiContext.java b/core/src/main/java/org/jclouds/rest/ApiContext.java new file mode 100644 index 0000000000..8aac086d86 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/ApiContext.java @@ -0,0 +1,45 @@ +/** + * 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.rest; + +import org.jclouds.Context; +import org.jclouds.rest.internal.ApiContextImpl; + +import com.google.inject.ImplementedBy; + +/** + * Represents an authenticated context to the cloud. + * + *

Note

Please issue {@link #close()} when you are finished with this context in order to + * release resources. + * + * + * @author Adrian Cole + */ +@ImplementedBy(ApiContextImpl.class) +public interface ApiContext
extends Context { + + + /** + * low-level api to the cloud. Threadsafe implementations will return a singleton. + * + * @return a connection to the cloud where all methods block + */ + A getApi(); +} diff --git a/core/src/main/java/org/jclouds/rest/ConfiguresRestContext.java b/core/src/main/java/org/jclouds/rest/ConfiguresHttpApi.java similarity index 89% rename from core/src/main/java/org/jclouds/rest/ConfiguresRestContext.java rename to core/src/main/java/org/jclouds/rest/ConfiguresHttpApi.java index 021bc4c38a..1d39d79eb4 100644 --- a/core/src/main/java/org/jclouds/rest/ConfiguresRestContext.java +++ b/core/src/main/java/org/jclouds/rest/ConfiguresHttpApi.java @@ -25,13 +25,13 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; /** - * designates the module configures a Client to a cloud. + * designates the module configures a top-level api which is annotated with http methods. * * @author Adrian Cole * */ @Retention(RUNTIME) @Target(TYPE) -public @interface ConfiguresRestContext { +public @interface ConfiguresHttpApi { } diff --git a/core/src/main/java/org/jclouds/rest/HttpApiMetadata.java b/core/src/main/java/org/jclouds/rest/HttpApiMetadata.java new file mode 100644 index 0000000000..759888128b --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/HttpApiMetadata.java @@ -0,0 +1,47 @@ +/** + * 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.rest; + +import org.jclouds.apis.ApiMetadata; + +import com.google.common.annotations.Beta; + +/** + * + * @author Adrian Cole + * @since 1.6 + * @see ConfiguresHttpApi + */ +@Beta +public interface HttpApiMetadata extends ApiMetadata { + + public static interface Builder> extends ApiMetadata.Builder { + + /** + * @see ApiMetadata#getApi() + */ + T javaApi(Class api); + } + + /** + * + * @return the type of the api which has http annotations on its methods. + */ + Class getApi(); +} diff --git a/core/src/main/java/org/jclouds/rest/HttpAsyncClient.java b/core/src/main/java/org/jclouds/rest/HttpAsyncClient.java index a955df563b..9d86e35420 100644 --- a/core/src/main/java/org/jclouds/rest/HttpAsyncClient.java +++ b/core/src/main/java/org/jclouds/rest/HttpAsyncClient.java @@ -43,7 +43,10 @@ import com.google.common.util.concurrent.ListenableFuture; * Simple rest client * * @author Adrian Cole + * @deprecated will be removed in jclouds 1.7, as async interfaces are no longer + * supported. */ +@Deprecated public interface HttpAsyncClient { /** * @see HttpClient#put diff --git a/core/src/main/java/org/jclouds/rest/HttpClient.java b/core/src/main/java/org/jclouds/rest/HttpClient.java index ab9eca183f..7e6a720abb 100644 --- a/core/src/main/java/org/jclouds/rest/HttpClient.java +++ b/core/src/main/java/org/jclouds/rest/HttpClient.java @@ -21,9 +21,21 @@ package org.jclouds.rest; import java.io.InputStream; import java.net.URI; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; + +import org.jclouds.Fallbacks.FalseOnNotFoundOr404; +import org.jclouds.Fallbacks.NullOnNotFoundOr404; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; +import org.jclouds.http.functions.ParseETagHeader; import org.jclouds.io.Payload; +import org.jclouds.rest.annotations.EndpointParam; +import org.jclouds.rest.annotations.Fallback; +import org.jclouds.rest.annotations.ResponseParser; /** * Simple client @@ -31,6 +43,34 @@ import org.jclouds.io.Payload; * @author Adrian Cole */ public interface HttpClient { + /** + * @return eTag + */ + @PUT + @ResponseParser(ParseETagHeader.class) + String put(@EndpointParam URI location, Payload payload); + + /** + * @return eTag + */ + @POST + @ResponseParser(ParseETagHeader.class) + String post(@EndpointParam URI location, Payload payload); + + /** + * @see HttpClient#exists + */ + @HEAD + @Fallback(FalseOnNotFoundOr404.class) + boolean exists(@EndpointParam URI location); + + /** + * @return null if the resource didn't exist. + */ + @GET + @Fallback(NullOnNotFoundOr404.class) + InputStream get(@EndpointParam URI location); + /** * * @param request @@ -38,25 +78,11 @@ public interface HttpClient { */ HttpResponse invoke(HttpRequest request); - /** - * @return eTag - */ - String put(URI location, Payload payload); - - /** - * @return eTag - */ - String post(URI location, Payload payload); - - boolean exists(URI location); - - /** - * @return null if the resource didn't exist. - */ - InputStream get(URI location); - /** * @return false if the resource didn't exist. */ - boolean delete(URI location); + @DELETE + @Fallback(FalseOnNotFoundOr404.class) + boolean delete(@EndpointParam URI location); + } diff --git a/core/src/main/java/org/jclouds/rest/RestApiMetadata.java b/core/src/main/java/org/jclouds/rest/RestApiMetadata.java index 7798cd1af3..7acb0564ed 100644 --- a/core/src/main/java/org/jclouds/rest/RestApiMetadata.java +++ b/core/src/main/java/org/jclouds/rest/RestApiMetadata.java @@ -21,12 +21,17 @@ package org.jclouds.rest; import org.jclouds.apis.ApiMetadata; import com.google.common.annotations.Beta; +import com.google.common.util.concurrent.ListenableFuture; /** * * @author Adrian Cole * @since 1.5 + * + * @deprecated please use {@link HttpApiMetadata} as + * async interface will be removed in jclouds 1.7. */ +@Deprecated @Beta public interface RestApiMetadata extends ApiMetadata { diff --git a/core/src/main/java/org/jclouds/rest/Utils.java b/core/src/main/java/org/jclouds/rest/Utils.java index c5bf450324..44695acdcf 100644 --- a/core/src/main/java/org/jclouds/rest/Utils.java +++ b/core/src/main/java/org/jclouds/rest/Utils.java @@ -75,11 +75,22 @@ public interface Utils { */ Json json(); + /** + * + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. + * @see #getHttpClient() + */ + @Deprecated HttpAsyncClient getHttpAsyncClient(); /** - * #see #getHttpAsyncClient + * + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. + * @see #http() */ + @Deprecated HttpAsyncClient asyncHttp(); HttpClient getHttpClient(); diff --git a/core/src/main/java/org/jclouds/rest/config/AsyncHttpApiProvider.java b/core/src/main/java/org/jclouds/rest/config/AnnotatedHttpApiProvider.java similarity index 74% rename from core/src/main/java/org/jclouds/rest/config/AsyncHttpApiProvider.java rename to core/src/main/java/org/jclouds/rest/config/AnnotatedHttpApiProvider.java index 2765f27349..40ec61cdcd 100644 --- a/core/src/main/java/org/jclouds/rest/config/AsyncHttpApiProvider.java +++ b/core/src/main/java/org/jclouds/rest/config/AnnotatedHttpApiProvider.java @@ -28,27 +28,26 @@ import org.jclouds.rest.internal.DelegatesToInvocationFunction; import com.google.common.base.Function; import com.google.inject.Provider; -import com.google.inject.TypeLiteral; /** * * @author Adrian Cole */ @Singleton -public class AsyncHttpApiProvider implements Provider { - private final Class asyncApiType; +public class AnnotatedHttpApiProvider implements Provider { + private final Class annotatedApiType; private final DelegatesToInvocationFunction> httpInvoker; @Inject - private AsyncHttpApiProvider(DelegatesToInvocationFunction> httpInvoker, - TypeLiteral asyncApiType) { + private AnnotatedHttpApiProvider(DelegatesToInvocationFunction> httpInvoker, + Class annotatedApiType) { this.httpInvoker = httpInvoker; - this.asyncApiType = asyncApiType.getRawType(); + this.annotatedApiType = annotatedApiType; } @SuppressWarnings("unchecked") @Override public A get() { - return (A) Proxy.newProxyInstance(asyncApiType.getClassLoader(), new Class[] { asyncApiType }, httpInvoker); + return (A) Proxy.newProxyInstance(annotatedApiType.getClassLoader(), new Class[] { annotatedApiType }, httpInvoker); } } diff --git a/core/src/main/java/org/jclouds/rest/config/AnnotatedMappedHttpApiProvider.java b/core/src/main/java/org/jclouds/rest/config/AnnotatedMappedHttpApiProvider.java new file mode 100644 index 0000000000..d51395d82c --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/config/AnnotatedMappedHttpApiProvider.java @@ -0,0 +1,60 @@ +/** + * 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.rest.config; + +import java.lang.reflect.Proxy; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.internal.DelegatesToInvocationFunction; +import org.jclouds.rest.internal.DelegatesToPotentiallyMappedInvocationFunction; + +import com.google.common.base.Function; +import com.google.inject.Provider; +import com.google.inject.TypeLiteral; + +/** + * + * @author Adrian Cole + * @deprecated please use {@link DelegatesToInvocationFunction} as async + * interface will be removed in jclouds 1.7. + */ +@Deprecated +@Singleton +public class AnnotatedMappedHttpApiProvider implements Provider { + private final Class annotatedApiType; + private final DelegatesToPotentiallyMappedInvocationFunction> httpInvoker; + + @Inject + private AnnotatedMappedHttpApiProvider( + DelegatesToPotentiallyMappedInvocationFunction> httpInvoker, + TypeLiteral annotatedApiType) { + this.httpInvoker = httpInvoker; + this.annotatedApiType = annotatedApiType.getRawType(); + } + + @SuppressWarnings("unchecked") + @Override + public A get() { + return (A) Proxy.newProxyInstance(annotatedApiType.getClassLoader(), new Class[] { annotatedApiType }, + httpInvoker); + } +} diff --git a/core/src/main/java/org/jclouds/rest/config/BinderUtils.java b/core/src/main/java/org/jclouds/rest/config/BinderUtils.java index 4cf560c9f9..85c1d181d9 100644 --- a/core/src/main/java/org/jclouds/rest/config/BinderUtils.java +++ b/core/src/main/java/org/jclouds/rest/config/BinderUtils.java @@ -31,7 +31,33 @@ import com.google.inject.TypeLiteral; public class BinderUtils { /** - * adds an explicit binding for {@code async} by parsing its annotations. Then. adds an explicit binding for an + * adds an explicit binding for {@code async} by parsing its annotations. + * + * @param + * sync interface that blocks + * @param + * api type with http annotations + * @param binder + * guice binder + * @param api + * type with http annotations + */ + public static void bindHttpApi(Binder binder, Class api) { + bindClass(binder, api); + bindAnnotatedHttpApiProvider(binder, api); + } + + @SuppressWarnings("unchecked") + private static void bindAnnotatedHttpApiProvider(Binder binder, Class annotated) { + TypeToken> token = new TypeToken>() { + private static final long serialVersionUID = 1L; + }.where(new TypeParameter() { + }, annotated); + binder.bind(annotated).toProvider(TypeLiteral.class.cast(TypeLiteral.get(token.getType()))); + } + + /** + * adds an explicit binding for {@code async} by parsing its annotations. Then, adds an explicit binding for an * interface which synchronously blocks on similar calls to an {@code async} type. * * @param @@ -44,52 +70,79 @@ public class BinderUtils { * type interface that blocks * @param async * type type that returns {@link ListenableFuture} + * + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. */ - public static void bindHttpApi(Binder binder, Class sync, Class async) { + @Deprecated + public static void bindMappedHttpApi(Binder binder, Class sync, Class async) { bindClass(binder, sync); bindClass(binder, async); - bindAsyncHttpApiProvider(binder, async); + bindAnnotatedMappedHttpApiProvider(binder, async); bindHttpApiProvider(binder, sync, async); } - + + /** + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. + */ + @Deprecated @SuppressWarnings("unchecked") - private static void bindAsyncHttpApiProvider(Binder binder, Class async) { - TypeToken> token = new TypeToken>() { + private static void bindAnnotatedMappedHttpApiProvider(Binder binder, Class annotated) { + TypeToken> token = new TypeToken>() { private static final long serialVersionUID = 1L; }.where(new TypeParameter() { - }, async); - binder.bind(async).toProvider(TypeLiteral.class.cast(TypeLiteral.get(token.getType()))); + }, annotated); + binder.bind(annotated).toProvider(TypeLiteral.class.cast(TypeLiteral.get(token.getType()))); } - + + /** + * + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. + */ + @Deprecated @SuppressWarnings("unchecked") private static void bindHttpApiProvider(Binder binder, Class sync, Class async) { - TypeToken> token = new TypeToken>() { + TypeToken> token = new TypeToken>() { private static final long serialVersionUID = 1L; }.where(new TypeParameter() { }, sync).where(new TypeParameter() { }, async); binder.bind(sync).toProvider(TypeLiteral.class.cast(TypeLiteral.get(token.getType()))); } + /** - * adds an explicit binding for an interface which synchronously blocks on similar calls to an {@code async} type. + * adds an explicit binding for an interface which synchronously blocks on + * similar calls to an {@code async} type. * * @param * sync interface that blocks * @param - * async type where all methods have same args as {@code sync}, but returns {@link ListenableFuture} + * async type where all methods have same args as {@code sync}, but + * returns {@link ListenableFuture} * @param binder * guice binder * @param sync * type interface that blocks * @param async * type type that returns {@link ListenableFuture} + * + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. */ - public static void bindBlockingApi(Binder binder, Class sync, Class async) { + @Deprecated + public static void bindMappedApi(Binder binder, Class sync, Class async) { bindClass(binder, sync); bindClass(binder, async); bindCallGetOnFutures(binder, sync, async); } + /** + * @deprecated will be removed in jclouds 1.7, as async interfaces are no + * longer supported. + */ + @Deprecated @SuppressWarnings("unchecked") private static void bindCallGetOnFutures(Binder binder, Class sync, Class async) { TypeToken> token = new TypeToken>() { diff --git a/core/src/main/java/org/jclouds/rest/config/CallGetOnFuturesProvider.java b/core/src/main/java/org/jclouds/rest/config/CallGetOnFuturesProvider.java index 27fb53c93d..30bb4f23b7 100644 --- a/core/src/main/java/org/jclouds/rest/config/CallGetOnFuturesProvider.java +++ b/core/src/main/java/org/jclouds/rest/config/CallGetOnFuturesProvider.java @@ -24,7 +24,7 @@ import java.lang.reflect.Proxy; import javax.inject.Inject; import javax.inject.Singleton; -import org.jclouds.rest.internal.DelegatesToInvocationFunction; +import org.jclouds.rest.internal.DelegatesToPotentiallyMappedInvocationFunction; import org.jclouds.rest.internal.InvokeAndCallGetOnFutures; import com.google.common.cache.Cache; @@ -33,20 +33,22 @@ import com.google.inject.Provider; /** * @author Adrian Cole + * @deprecated will be removed in jclouds 1.7, as async interfaces are no longer supported. */ +@Deprecated @Singleton public class CallGetOnFuturesProvider implements Provider { private final Class apiType; - private final DelegatesToInvocationFunction> syncInvoker; + private final DelegatesToPotentiallyMappedInvocationFunction> syncInvoker; @Inject private CallGetOnFuturesProvider(Cache, Invokable> invokables, - DelegatesToInvocationFunction> syncInvoker, Class apiType, + DelegatesToPotentiallyMappedInvocationFunction> syncInvoker, Class apiType, Class asyncApiType) { this.syncInvoker = syncInvoker; this.apiType = apiType; - RestModule.putInvokables(apiType, asyncApiType, invokables); + MappedHttpInvocationModule.putInvokables(apiType, asyncApiType, invokables); } @SuppressWarnings("unchecked") diff --git a/core/src/main/java/org/jclouds/rest/config/HttpApiModule.java b/core/src/main/java/org/jclouds/rest/config/HttpApiModule.java new file mode 100644 index 0000000000..5c0e70b044 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/config/HttpApiModule.java @@ -0,0 +1,102 @@ +/** + * 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.rest.config; + +import static org.jclouds.reflect.Types2.checkBound; +import static org.jclouds.rest.config.BinderUtils.bindHttpApi; + +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.ConfiguresHttpApi; +import org.jclouds.rest.HttpAsyncClient; +import org.jclouds.rest.HttpClient; +import org.jclouds.rest.internal.InvokeHttpMethod; + +import com.google.common.base.Function; +import com.google.common.reflect.TypeToken; +import com.google.inject.TypeLiteral; + +/** + * + * @author Adrian Cole + */ +@ConfiguresHttpApi +public class HttpApiModule extends RestModule { + protected final Class api; + + /** + * Note that this ctor requires that you instantiate w/resolved generic + * params. For example, via a subclass of a bound type, or natural + * instantiation w/resolved type params. + */ + @SuppressWarnings("unchecked") + protected HttpApiModule() { + this.api = Class.class.cast(checkBound(new TypeToken(getClass()) { + private static final long serialVersionUID = 1L; + }).getRawType()); + } + + public HttpApiModule(Class api) { + this.api = api; + } + + @Override + protected void configure() { + super.configure(); + bind(new TypeLiteral>() { + }).to(InvokeHttpMethod.class); + bindHttpApi(binder(), api); + bindHttpApi(binder(), HttpClient.class); + // TODO: remove when references are gone + bindHttpApi(binder(), HttpAsyncClient.class); + bindErrorHandlers(); + bindRetryHandlers(); + } + + /** + * overrides this to change the default retry handlers for the http engine + * + * ex. + * + *
+    * bind(HttpRetryHandler.class).annotatedWith(Redirection.class).to(AWSRedirectionRetryHandler.class);
+    * bind(HttpRetryHandler.class).annotatedWith(ClientError.class).to(AWSClientErrorRetryHandler.class);
+    * 
+ * + */ + protected void bindRetryHandlers() { + } + + /** + * overrides this to change the default error handlers for the http engine + * + * ex. + * + *
+    * bind(HttpErrorHandler.class).annotatedWith(Redirection.class).to(ParseAWSErrorFromXmlContent.class);
+    * bind(HttpErrorHandler.class).annotatedWith(ClientError.class).to(ParseAWSErrorFromXmlContent.class);
+    * bind(HttpErrorHandler.class).annotatedWith(ServerError.class).to(ParseAWSErrorFromXmlContent.class);
+    * 
+ * + * + */ + protected void bindErrorHandlers() { + + } + +} diff --git a/core/src/main/java/org/jclouds/rest/config/HttpApiProvider.java b/core/src/main/java/org/jclouds/rest/config/MappedHttpApiProvider.java similarity index 69% rename from core/src/main/java/org/jclouds/rest/config/HttpApiProvider.java rename to core/src/main/java/org/jclouds/rest/config/MappedHttpApiProvider.java index 4df9f77e63..1885141d70 100644 --- a/core/src/main/java/org/jclouds/rest/config/HttpApiProvider.java +++ b/core/src/main/java/org/jclouds/rest/config/MappedHttpApiProvider.java @@ -25,7 +25,7 @@ import javax.inject.Inject; import javax.inject.Singleton; import org.jclouds.reflect.Invocation; -import org.jclouds.rest.internal.DelegatesToInvocationFunction; +import org.jclouds.rest.internal.DelegatesToPotentiallyMappedInvocationFunction; import com.google.common.base.Function; import com.google.common.cache.Cache; @@ -35,18 +35,20 @@ import com.google.inject.Provider; /** * * @author Adrian Cole + * @deprecated will be removed in jclouds 1.7; use {@link AnnotatedHttpApiProvider} */ +@Deprecated @Singleton -public class HttpApiProvider implements Provider { +public class MappedHttpApiProvider implements Provider { private final Class apiType; - private final DelegatesToInvocationFunction> httpInvoker; + private final DelegatesToPotentiallyMappedInvocationFunction> httpInvoker; @Inject - private HttpApiProvider(Cache, Invokable> invokables, - DelegatesToInvocationFunction> httpInvoker, Class apiType, Class
asyncApiType) { + private MappedHttpApiProvider(Cache, Invokable> invokables, + DelegatesToPotentiallyMappedInvocationFunction> httpInvoker, Class apiType, Class asyncApiType) { this.httpInvoker = httpInvoker; this.apiType = apiType; - RestModule.putInvokables(apiType, asyncApiType, invokables); + MappedHttpInvocationModule.putInvokables(apiType, asyncApiType, invokables); } @SuppressWarnings("unchecked") diff --git a/core/src/main/java/org/jclouds/rest/config/MappedHttpInvocationModule.java b/core/src/main/java/org/jclouds/rest/config/MappedHttpInvocationModule.java new file mode 100644 index 0000000000..ffac5fa800 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/config/MappedHttpInvocationModule.java @@ -0,0 +1,150 @@ +/** + * 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.rest.config; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Iterables.toArray; +import static com.google.common.collect.Iterables.transform; +import static org.jclouds.reflect.Reflection2.method; +import static org.jclouds.reflect.Reflection2.methods; + +import java.io.Closeable; +import java.util.Map; + +import javax.inject.Singleton; + +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.HttpAsyncClient; +import org.jclouds.rest.HttpClient; +import org.jclouds.rest.internal.DelegatesToInvocationFunction; +import org.jclouds.rest.internal.DelegatesToPotentiallyMappedInvocationFunction; +import org.jclouds.rest.internal.InvokeAndCallGetOnFutures; +import org.jclouds.rest.internal.InvokeMappedHttpMethod; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableMap; +import com.google.common.reflect.Invokable; +import com.google.common.reflect.Parameter; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import com.google.inject.TypeLiteral; + +/** + * supports sync-async mapping + * + * @deprecated will be removed in jclouds 1.7; use {@link HttpApiModule} + */ +@Deprecated +public class MappedHttpInvocationModule extends AbstractModule { + protected final Map, Class> sync2Async; + + public MappedHttpInvocationModule() { + this(ImmutableMap., Class> of()); + } + + public MappedHttpInvocationModule(Map, Class> sync2Async) { + this.sync2Async = sync2Async; + } + + @Override + protected void configure() { + bind(new TypeLiteral, Class>>() { + }).toInstance(sync2Async); + bind(new TypeLiteral>() { + }).to(InvokeMappedHttpMethod.class); + org.jclouds.rest.config.BinderUtils.bindMappedHttpApi(binder(), HttpClient.class, HttpAsyncClient.class); + } + + /** + * seeds well-known invokables. + */ + @Provides + @Singleton + protected Cache, Invokable> seedKnownSync2AsyncInvokables() { + return seedKnownSync2AsyncInvokables(sync2Async); + } + + /** + * function view of above + * + * @see InvokeAndCallGetOnFutures + * @see InvokeMappedHttpMethod + */ + @Provides + @Singleton + protected Function sync2async(final Cache, Invokable> cache) { + return new Function() { + public Invocation apply(Invocation in) { + return Invocation.create( + checkNotNull(cache.getIfPresent(in.getInvokable()), "invokable %s not in %s", in.getInvokable(), + cache), in.getArgs()); + } + }; + } + + @VisibleForTesting + static Cache, Invokable> seedKnownSync2AsyncInvokables(Map, Class> sync2Async) { + Cache, Invokable> sync2AsyncBuilder = CacheBuilder.newBuilder().build(); + putInvokables(HttpClient.class, HttpAsyncClient.class, sync2AsyncBuilder); + for (Map.Entry, Class> entry : sync2Async.entrySet()) { + putInvokables(entry.getKey(), entry.getValue(), sync2AsyncBuilder); + } + return sync2AsyncBuilder; + } + + // accessible for ClientProvider + public static void putInvokables(Class sync, Class async, Cache, Invokable> cache) { + for (Invokable invoked : methods(sync)) { + Invokable delegatedMethod = method(async, invoked.getName(), getParameterTypes(invoked)); + checkArgument(delegatedMethod.getExceptionTypes().equals(invoked.getExceptionTypes()) + || isCloseable(delegatedMethod), "invoked %s has different typed exceptions than target %s", invoked, + delegatedMethod); + cache.put(invoked, delegatedMethod); + } + } + + /** + * In JDK7 Closeable.close is declared in AutoCloseable, which throws + * Exception vs IOException, so we have to be more lenient about exception + * type declarations. + * + *

note

+ * + * This will be refactored out when we delete Async code in jclouds 1.7. + */ + private static boolean isCloseable(Invokable delegatedMethod) { + return "close".equals(delegatedMethod.getName()) + && Closeable.class.isAssignableFrom(delegatedMethod.getDeclaringClass()); + } + + /** + * for portability with {@link Class#getMethod(String, Class...)} + */ + private static Class[] getParameterTypes(Invokable in) { + return toArray(transform(checkNotNull(in, "invokable").getParameters(), new Function>() { + public Class apply(Parameter input) { + return input.getType().getRawType(); + } + }), Class.class); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/config/RestClientModule.java b/core/src/main/java/org/jclouds/rest/config/RestClientModule.java index dfa63c1770..a206ce0e87 100644 --- a/core/src/main/java/org/jclouds/rest/config/RestClientModule.java +++ b/core/src/main/java/org/jclouds/rest/config/RestClientModule.java @@ -18,10 +18,9 @@ */ package org.jclouds.rest.config; -import static com.google.common.base.Preconditions.checkState; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.reflect.Types2.checkBound; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; -import java.lang.reflect.TypeVariable; import java.util.Map; import org.jclouds.rest.ConfiguresRestClient; @@ -32,18 +31,23 @@ import com.google.common.reflect.TypeToken; /** * * @author Adrian Cole + * + * @deprecated will be removed in jclouds 1.7; use {@link HttpApiModule} */ +@Deprecated @ConfiguresRestClient public class RestClientModule extends RestModule { protected final TypeToken syncClientType; protected final TypeToken
asyncClientType; + private final MappedHttpInvocationModule invocationModule; + /** * Note that this ctor requires that you instantiate w/resolved generic params. For example, via * a subclass of a bound type, or natural instantiation w/resolved type params. */ protected RestClientModule(Map, Class> sync2Async) { - super(sync2Async); + this.invocationModule = new MappedHttpInvocationModule(sync2Async); this.syncClientType = checkBound(new TypeToken(getClass()) { private static final long serialVersionUID = 1L; }); @@ -51,15 +55,6 @@ public class RestClientModule extends RestModule { private static final long serialVersionUID = 1L; }); } - - /** - * @throws IllegalStateException if the type is an instanceof {@link TypeVariable} - */ - private static TypeToken checkBound(TypeToken type) throws IllegalStateException { - checkState(!(type.getType() instanceof TypeVariable), - "unbound type variable: %s, use ctor that explicitly assigns this", type); - return type; - } /** * @see #RestClientModule(Map) @@ -79,7 +74,7 @@ public class RestClientModule extends RestModule { * only necessary when type params are not resolvable at runtime. */ public RestClientModule(TypeToken syncClientType, TypeToken asyncClientType, Map, Class> sync2Async) { - super(sync2Async); + this.invocationModule = new MappedHttpInvocationModule(sync2Async); this.syncClientType = checkBound(syncClientType); this.asyncClientType = checkBound(asyncClientType); } @@ -87,7 +82,8 @@ public class RestClientModule extends RestModule { @Override protected void configure() { super.configure(); - bindHttpApi(binder(), syncClientType.getRawType(), asyncClientType.getRawType()); + install(invocationModule); + bindMappedHttpApi(binder(), syncClientType.getRawType(), asyncClientType.getRawType()); bindErrorHandlers(); bindRetryHandlers(); } diff --git a/core/src/main/java/org/jclouds/rest/config/RestModule.java b/core/src/main/java/org/jclouds/rest/config/RestModule.java index db7f67b07a..fb16e178ec 100644 --- a/core/src/main/java/org/jclouds/rest/config/RestModule.java +++ b/core/src/main/java/org/jclouds/rest/config/RestModule.java @@ -18,23 +18,13 @@ */ package org.jclouds.rest.config; -import static com.google.common.base.Preconditions.checkArgument; -import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.collect.Iterables.toArray; -import static com.google.common.collect.Iterables.transform; import static com.google.common.util.concurrent.Atomics.newReference; -import static org.jclouds.reflect.Reflection2.method; -import static org.jclouds.reflect.Reflection2.methods; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; -import java.io.Closeable; import java.net.Proxy; import java.net.URI; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import javax.inject.Singleton; - import org.jclouds.fallbacks.MapHttp4xxCodesToExceptions; import org.jclouds.functions.IdentityFunction; import org.jclouds.http.HttpRequest; @@ -46,24 +36,14 @@ import org.jclouds.location.config.LocationModule; import org.jclouds.proxy.ProxyForURI; import org.jclouds.reflect.Invocation; import org.jclouds.rest.AuthorizationException; -import org.jclouds.rest.HttpAsyncClient; -import org.jclouds.rest.HttpClient; import org.jclouds.rest.binders.BindToJsonPayloadWrappedWith; -import org.jclouds.rest.internal.InvokeHttpMethod; import org.jclouds.rest.internal.RestAnnotationProcessor; import org.jclouds.rest.internal.TransformerForRequest; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Supplier; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.ImmutableMap; -import com.google.common.reflect.Invokable; -import com.google.common.reflect.Parameter; import com.google.inject.AbstractModule; -import com.google.inject.Provides; import com.google.inject.TypeLiteral; import com.google.inject.assistedinject.FactoryModuleBuilder; @@ -72,109 +52,25 @@ public class RestModule extends AbstractModule { public static final TypeLiteral> URI_SUPPLIER_TYPE = new TypeLiteral>() { }; - protected final Map, Class> sync2Async; protected final AtomicReference authException = newReference(); - public RestModule() { - this(ImmutableMap., Class> of()); - } - - public RestModule(Map, Class> sync2Async) { - this.sync2Async = sync2Async; - } - - /** - * seeds well-known invokables. - */ - @Provides - @Singleton - protected Cache, Invokable> seedKnownSync2AsyncInvokables() { - return seedKnownSync2AsyncInvokables(sync2Async); - } - - /** - * function view of above - */ - @Provides - @Singleton - protected Function sync2async(final Cache, Invokable> cache) { - return new Function() { - public Invocation apply(Invocation in) { - return Invocation.create( - checkNotNull(cache.getIfPresent(in.getInvokable()), "invokable %s not in %s", in.getInvokable(), - cache), in.getArgs()); - } - }; - } - - @VisibleForTesting - static Cache, Invokable> seedKnownSync2AsyncInvokables(Map, Class> sync2Async) { - Cache, Invokable> sync2AsyncBuilder = CacheBuilder.newBuilder().build(); - putInvokables(HttpClient.class, HttpAsyncClient.class, sync2AsyncBuilder); - for (Map.Entry, Class> entry : sync2Async.entrySet()) { - putInvokables(entry.getKey(), entry.getValue(), sync2AsyncBuilder); - } - return sync2AsyncBuilder; - } - - // accessible for ClientProvider - public static void putInvokables(Class sync, Class async, Cache, Invokable> cache) { - for (Invokable invoked : methods(sync)) { - Invokable delegatedMethod = method(async, invoked.getName(), getParameterTypes(invoked)); - checkArgument( - delegatedMethod.getExceptionTypes().equals(invoked.getExceptionTypes()) || isCloseable(delegatedMethod), - "invoked %s has different typed exceptions than target %s", invoked, delegatedMethod); - cache.put(invoked, delegatedMethod); - } - } - - /** - * In JDK7 Closeable.close is declared in AutoCloseable, which throws - * Exception vs IOException, so we have to be more lenient about exception - * type declarations. - * - *

note

- * - * This will be refactored out when we delete Async code in jclouds 1.7. - */ - private static boolean isCloseable(Invokable delegatedMethod) { - return "close".equals(delegatedMethod.getName()) - && Closeable.class.isAssignableFrom(delegatedMethod.getDeclaringClass()); - } - - /** - * for portability with {@link Class#getMethod(String, Class...)} - */ - private static Class[] getParameterTypes(Invokable in) { - return toArray(transform(checkNotNull(in, "invokable").getParameters(), new Function>() { - public Class apply(Parameter input) { - return input.getType().getRawType(); - } - }), Class.class); - } - protected void installLocations() { install(new LocationModule()); } @Override protected void configure() { - bind(new TypeLiteral, Class>>() { - }).toInstance(sync2Async); install(new SaxParserModule()); install(new GsonModule()); install(new SetCaller.Module()); install(new FactoryModuleBuilder().build(BindToJsonPayloadWrappedWith.Factory.class)); bind(new TypeLiteral>>() { }).to(TransformerForRequest.class); - bind(new TypeLiteral>() { - }).to(InvokeHttpMethod.class); bind(new TypeLiteral>() { }).to(MapHttp4xxCodesToExceptions.class); bind(new TypeLiteral>() { }).to(RestAnnotationProcessor.class); bind(IdentityFunction.class).toInstance(IdentityFunction.INSTANCE); - bindHttpApi(binder(), HttpClient.class, HttpAsyncClient.class); // this will help short circuit scenarios that can otherwise lock out users bind(new TypeLiteral>() { }).toInstance(authException); diff --git a/core/src/main/java/org/jclouds/rest/internal/ApiContextImpl.java b/core/src/main/java/org/jclouds/rest/internal/ApiContextImpl.java new file mode 100644 index 0000000000..1296f43d7d --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/internal/ApiContextImpl.java @@ -0,0 +1,60 @@ +/** + * 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.rest.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import javax.inject.Inject; + +import org.jclouds.annotations.Name; +import org.jclouds.domain.Credentials; +import org.jclouds.internal.ContextImpl; +import org.jclouds.lifecycle.Closer; +import org.jclouds.location.Provider; +import org.jclouds.providers.ProviderMetadata; +import org.jclouds.rest.ApiContext; +import org.jclouds.rest.Utils; + +import com.google.common.base.Supplier; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; + +/** + * @author Adrian Cole +*/ +@Singleton +public class ApiContextImpl
extends ContextImpl implements ApiContext { + + private final A api; + + @Inject + protected ApiContextImpl(@Name String name, ProviderMetadata providerMetadata, + @Provider Supplier creds, Utils utils, Closer closer, Injector injector, TypeLiteral api) { + super(name, providerMetadata, creds, utils, closer); + checkNotNull(injector, "injector"); + this.api = injector.getInstance(Key.get(checkNotNull(api, "api"))); + } + + @Override + public A getApi() { + return api; + } +} diff --git a/core/src/main/java/org/jclouds/rest/internal/BaseHttpApiMetadata.java b/core/src/main/java/org/jclouds/rest/internal/BaseHttpApiMetadata.java new file mode 100644 index 0000000000..19794a476b --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/internal/BaseHttpApiMetadata.java @@ -0,0 +1,121 @@ +/** + * 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.rest.internal; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.jclouds.reflect.Reflection2.typeToken; +import static org.jclouds.reflect.Types2.checkBound; + +import java.util.Properties; + +import org.jclouds.apis.ApiMetadata; +import org.jclouds.apis.internal.BaseApiMetadata; +import org.jclouds.rest.ApiContext; +import org.jclouds.rest.HttpApiMetadata; + +import com.google.common.annotations.Beta; +import com.google.common.base.Objects.ToStringHelper; +import com.google.common.reflect.TypeParameter; +import com.google.common.reflect.TypeToken; + +/** + * Useful in creating http apis. + * + * @author Adrian Cole + */ +@Beta +public abstract class BaseHttpApiMetadata extends BaseApiMetadata implements HttpApiMetadata { + + protected final Class api; + + protected BaseHttpApiMetadata(Builder builder) { + super(builder); + this.api = checkNotNull(builder.api, "api"); + } + + public static Properties defaultProperties() { + Properties props = BaseApiMetadata.defaultProperties(); + return props; + } + + public static TypeToken> contextToken(TypeToken apiToken) { + return new TypeToken>() { + private static final long serialVersionUID = 1L; + }.where(new TypeParameter() { + }, apiToken); + } + + public static abstract class Builder> extends BaseApiMetadata.Builder implements + HttpApiMetadata.Builder { + protected Class api; + + /** + * Note that this ctor requires that you instantiate w/resolved generic + * params. For example, via a subclass of a bound type, or natural + * instantiation w/resolved type params. + */ + @SuppressWarnings("unchecked") + protected Builder() { + this.api = Class.class.cast(checkBound(new TypeToken(getClass()) { + private static final long serialVersionUID = 1L; + }).getRawType()); + init(); + } + + protected Builder(Class api) { + this.api = checkNotNull(api, "api"); + init(); + } + + private void init() { + javaApi(api) + .name(api.getSimpleName()) + .context(contextToken(typeToken(api))) + .defaultProperties(BaseHttpApiMetadata.defaultProperties()); + } + + @Override + public T javaApi(Class api) { + this.api = checkNotNull(api, "api"); + return self(); + } + + @SuppressWarnings("unchecked") + @Override + public T fromApiMetadata(ApiMetadata in) { + if (in instanceof HttpApiMetadata) { + HttpApiMetadata http = HttpApiMetadata.class.cast(in); + javaApi(Class.class.cast(http.getApi())); + } + super.fromApiMetadata(in); + return self(); + } + + } + + @Override + public Class getApi() { + return api; + } + + @Override + protected ToStringHelper string() { + return super.string().add("api", getApi()); + } +} diff --git a/core/src/main/java/org/jclouds/rest/internal/BaseRestApiMetadata.java b/core/src/main/java/org/jclouds/rest/internal/BaseRestApiMetadata.java index 4c4c8990b0..c10efb5c3f 100644 --- a/core/src/main/java/org/jclouds/rest/internal/BaseRestApiMetadata.java +++ b/core/src/main/java/org/jclouds/rest/internal/BaseRestApiMetadata.java @@ -26,7 +26,6 @@ import java.util.Properties; import org.jclouds.apis.ApiMetadata; import org.jclouds.apis.internal.BaseApiMetadata; import org.jclouds.rest.RestApiMetadata; -import org.jclouds.rest.RestContext; import com.google.common.annotations.Beta; import com.google.common.base.Objects.ToStringHelper; @@ -37,6 +36,8 @@ import com.google.common.reflect.TypeToken; * Useful in creating rest apis. * * @author Adrian Cole + * @deprecated please use {@link BaseHttpApiMetadata} as + * async interface will be removed in jclouds 1.7. */ @Beta public abstract class BaseRestApiMetadata extends BaseApiMetadata implements RestApiMetadata { @@ -55,8 +56,8 @@ public abstract class BaseRestApiMetadata extends BaseApiMetadata implements Res return props; } - public static TypeToken> contextToken(TypeToken apiToken, TypeToken asyncApiToken) { - return new TypeToken>() { + public static TypeToken> contextToken(TypeToken apiToken, TypeToken asyncApiToken) { + return new TypeToken>() { private static final long serialVersionUID = 1L; }.where(new TypeParameter() { }, apiToken).where(new TypeParameter() { diff --git a/core/src/main/java/org/jclouds/rest/internal/DelegatesToInvocationFunction.java b/core/src/main/java/org/jclouds/rest/internal/DelegatesToInvocationFunction.java index 014b9116f5..f0a1055008 100644 --- a/core/src/main/java/org/jclouds/rest/internal/DelegatesToInvocationFunction.java +++ b/core/src/main/java/org/jclouds/rest/internal/DelegatesToInvocationFunction.java @@ -39,7 +39,6 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import javax.inject.Inject; import javax.inject.Qualifier; @@ -78,7 +77,7 @@ import com.google.inject.util.Types; * The function that implements this dynamic proxy */ @Beta -public final class DelegatesToInvocationFunction> implements +public class DelegatesToInvocationFunction> implements InvocationHandler { private static final Object[] NO_ARGS = {}; @@ -161,20 +160,18 @@ public final class DelegatesToInvocationFunction ownerType; - private final SetCaller setCaller; - private final Map, Class> syncToAsync; - private final Function> optionalConverter; - private final F methodInvoker; + protected final Injector injector; + protected final TypeToken ownerType; + protected final SetCaller setCaller; + protected final Function> optionalConverter; + protected final F methodInvoker; @Inject - DelegatesToInvocationFunction(Injector injector, SetCaller setCaller, Map, Class> syncToAsync, + DelegatesToInvocationFunction(Injector injector, SetCaller setCaller, Class ownerType, Function> optionalConverter, F methodInvoker) { this.injector = checkNotNull(injector, "injector"); this.ownerType = typeToken(checkNotNull(ownerType, "ownerType")); this.setCaller = checkNotNull(setCaller, "setCaller"); - this.syncToAsync = checkNotNull(syncToAsync, "syncToAsync"); this.optionalConverter = checkNotNull(optionalConverter, "optionalConverter"); this.methodInvoker = checkNotNull(methodInvoker, "methodInvoker"); } @@ -200,20 +197,12 @@ public final class DelegatesToInvocationFunction methodInvokerFor(Class returnType) { + protected Key methodInvokerFor(Class returnType) { switch (methodInvoker.getClass().getTypeParameters().length) { case 0: return Key.get(methodInvoker.getClass()); case 1: return Key.get(Types.newParameterizedType(methodInvoker.getClass(), returnType)); - case 2: - if (syncToAsync.containsValue(returnType)) - return Key.get(Types.newParameterizedType(methodInvoker.getClass(), returnType, returnType)); - return Key.get(Types.newParameterizedType( - methodInvoker.getClass(), - returnType, - checkNotNull(syncToAsync.get(returnType), "need async type of %s for %s", returnType, - methodInvoker.getClass()))); } throw new IllegalArgumentException(returnType + " has too many type parameters"); } diff --git a/core/src/main/java/org/jclouds/rest/internal/DelegatesToPotentiallyMappedInvocationFunction.java b/core/src/main/java/org/jclouds/rest/internal/DelegatesToPotentiallyMappedInvocationFunction.java new file mode 100644 index 0000000000..750e652a8b --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/internal/DelegatesToPotentiallyMappedInvocationFunction.java @@ -0,0 +1,74 @@ +/** + * 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.rest.internal; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Map; + +import javax.inject.Inject; + +import org.jclouds.reflect.Invocation; +import org.jclouds.reflect.InvocationSuccess; +import org.jclouds.rest.config.SetCaller; + +import com.google.common.annotations.Beta; +import com.google.common.base.Function; +import com.google.common.base.Optional; +import com.google.inject.Injector; +import com.google.inject.Key; +import com.google.inject.util.Types; + +/** + * @param + * The enclosing type of the interface that a dynamic proxy like this + * implements + * @param + * The function that implements this dynamic proxy + * + * @deprecated please use {@link DelegatesToInvocationFunction} as + * async interface will be removed in jclouds 1.7. + */ +@Deprecated +@Beta +public final class DelegatesToPotentiallyMappedInvocationFunction> extends + DelegatesToInvocationFunction { + private final Map, Class> syncToAsync; + + @Inject + DelegatesToPotentiallyMappedInvocationFunction(Injector injector, SetCaller setCaller, Class ownerType, + Function> optionalConverter, F methodInvoker, + Map, Class> syncToAsync) { + super(injector, setCaller, ownerType, optionalConverter, methodInvoker); + this.syncToAsync = checkNotNull(syncToAsync, "syncToAsync"); + } + + protected Key methodInvokerFor(Class returnType) { + if (methodInvoker.getClass().getTypeParameters().length == 2) { + if (syncToAsync.containsValue(returnType)) + return Key.get(Types.newParameterizedType(methodInvoker.getClass(), returnType, returnType)); + return Key.get(Types.newParameterizedType( + methodInvoker.getClass(), + returnType, + checkNotNull(syncToAsync.get(returnType), "need async type of %s for %s", returnType, + methodInvoker.getClass()))); + } + return super.methodInvokerFor(returnType); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java b/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java index cda07b0003..a132b8aa40 100644 --- a/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java +++ b/core/src/main/java/org/jclouds/rest/internal/InvokeAndCallGetOnFutures.java @@ -37,7 +37,9 @@ import com.google.common.util.concurrent.ListenableFuture; /** * * @author Adrian Cole + * @deprecated will be removed in jclouds 1.7, as async interfaces are no longer supported. */ +@Deprecated public final class InvokeAndCallGetOnFutures implements Function { @Resource diff --git a/core/src/main/java/org/jclouds/rest/internal/InvokeHttpMethod.java b/core/src/main/java/org/jclouds/rest/internal/InvokeHttpMethod.java index e97a01a794..7a358ddc08 100644 --- a/core/src/main/java/org/jclouds/rest/internal/InvokeHttpMethod.java +++ b/core/src/main/java/org/jclouds/rest/internal/InvokeHttpMethod.java @@ -20,19 +20,14 @@ package org.jclouds.rest.internal; import static com.google.common.base.Objects.equal; import static com.google.common.base.Objects.toStringHelper; -import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Throwables.propagate; -import static com.google.common.util.concurrent.Futures.transform; -import static com.google.common.util.concurrent.Futures.withFallback; import static java.util.concurrent.TimeUnit.NANOSECONDS; -import static org.jclouds.Constants.PROPERTY_USER_THREADS; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import javax.inject.Inject; -import javax.inject.Named; import org.jclouds.http.HttpCommand; import org.jclouds.http.HttpCommandExecutorService; @@ -47,9 +42,6 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Objects; import com.google.common.base.Optional; -import com.google.common.reflect.Invokable; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.TimeLimiter; import com.google.common.util.concurrent.UncheckedTimeoutException; @@ -61,24 +53,19 @@ public class InvokeHttpMethod implements Function { @Resource private Logger logger = Logger.NULL; - private final Function sync2async; private final Function annotationProcessor; private final HttpCommandExecutorService http; - private final ListeningExecutorService userExecutor; private final TimeLimiter timeLimiter; private final Function> transformerForRequest; private final InvocationConfig config; @Inject @VisibleForTesting - InvokeHttpMethod(Function sync2async, Function annotationProcessor, + InvokeHttpMethod(Function annotationProcessor, HttpCommandExecutorService http, Function> transformerForRequest, - TimeLimiter timeLimiter, InvocationConfig config, - @Named(PROPERTY_USER_THREADS) ListeningExecutorService userExecutor) { - this.sync2async = sync2async; + TimeLimiter timeLimiter, InvocationConfig config) { this.annotationProcessor = annotationProcessor; this.http = http; - this.userExecutor = userExecutor; this.timeLimiter = timeLimiter; this.transformerForRequest = transformerForRequest; this.config = config; @@ -86,32 +73,11 @@ public class InvokeHttpMethod implements Function { @Override public Object apply(Invocation in) { - if (isFuture(in.getInvokable())) { - return submit(in); - } - Invocation async = toAsync(in); - Optional timeoutNanos = config.getTimeoutNanos(async); + Optional timeoutNanos = config.getTimeoutNanos(in); if (timeoutNanos.isPresent()) { - return invokeWithTimeout(async, timeoutNanos.get()); + return invokeWithTimeout(in, timeoutNanos.get()); } - return invoke(async); - } - - /** - * submits the {@linkplain HttpCommand} associated with {@code invocation}, - * {@link #getTransformer(String, HttpCommand) parses its response}, and - * applies a {@link #getFallback(String, Invocation, HttpCommand) fallback} - * if a {@code Throwable} is encountered. Parsing and Fallback occur on the - * {@code userExecutor} thread. - */ - public ListenableFuture submit(Invocation invocation) { - String commandName = config.getCommandName(invocation); - HttpCommand command = toCommand(commandName, invocation); - Function transformer = getTransformer(commandName, command); - org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); - - logger.debug(">> submitting %s", commandName); - return withFallback(transform(http.submit(command), transformer, userExecutor), fallback); + return invoke(in); } /** @@ -224,17 +190,6 @@ public class InvokeHttpMethod implements Function { } } - /** - * looks up the corresponding {@code Invocation} that returns a - * {@code Future}. Only Invokables that return {@code Futures} are annotated - * in a way that can be parsed into an {@linkplain HttpRequest}. - */ - private Invocation toAsync(Invocation in) { - Invocation async = sync2async.apply(in); - checkState(isFuture(async.getInvokable()), "not a future: %s", async); - return async; - } - private HttpCommand toCommand(String commandName, Invocation invocation) { logger.trace(">> converting %s", commandName); HttpRequest request = annotationProcessor.apply(invocation); @@ -249,10 +204,6 @@ public class InvokeHttpMethod implements Function { return transformer; } - private boolean isFuture(Invokable in) { - return in.getReturnType().getRawType().equals(ListenableFuture.class); - } - @Override public boolean equals(Object o) { if (this == o) diff --git a/core/src/main/java/org/jclouds/rest/internal/InvokeMappedHttpMethod.java b/core/src/main/java/org/jclouds/rest/internal/InvokeMappedHttpMethod.java new file mode 100644 index 0000000000..09a0bca57b --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/internal/InvokeMappedHttpMethod.java @@ -0,0 +1,277 @@ +/** + * 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.rest.internal; + +import static com.google.common.base.Objects.equal; +import static com.google.common.base.Objects.toStringHelper; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Throwables.propagate; +import static com.google.common.util.concurrent.Futures.transform; +import static com.google.common.util.concurrent.Futures.withFallback; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.jclouds.Constants.PROPERTY_USER_THREADS; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Resource; +import javax.inject.Inject; +import javax.inject.Named; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpCommandExecutorService; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.logging.Logger; +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.InvocationContext; +import org.jclouds.rest.config.InvocationConfig; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.base.Optional; +import com.google.common.reflect.Invokable; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.TimeLimiter; +import com.google.common.util.concurrent.UncheckedTimeoutException; + +/** + * @author Adrian Cole + * @deprecated will be replaced in jclouds 1.7 with {@link InvokeHttpMethod}, as async interfaces are no longer supported. + */ +@Deprecated +public class InvokeMappedHttpMethod implements Function { + + @Resource + private Logger logger = Logger.NULL; + + private final Function sync2async; + private final Function annotationProcessor; + private final HttpCommandExecutorService http; + private final ListeningExecutorService userExecutor; + private final TimeLimiter timeLimiter; + private final Function> transformerForRequest; + private final InvocationConfig config; + + @Inject + @VisibleForTesting + InvokeMappedHttpMethod(Function sync2async, Function annotationProcessor, + HttpCommandExecutorService http, Function> transformerForRequest, + TimeLimiter timeLimiter, InvocationConfig config, + @Named(PROPERTY_USER_THREADS) ListeningExecutorService userExecutor) { + this.sync2async = sync2async; + this.annotationProcessor = annotationProcessor; + this.http = http; + this.userExecutor = userExecutor; + this.timeLimiter = timeLimiter; + this.transformerForRequest = transformerForRequest; + this.config = config; + } + + @Override + public Object apply(Invocation in) { + if (isFuture(in.getInvokable())) { + return submit(in); + } + Invocation async = toAsync(in); + Optional timeoutNanos = config.getTimeoutNanos(async); + if (timeoutNanos.isPresent()) { + return invokeWithTimeout(async, timeoutNanos.get()); + } + return invoke(async); + } + + /** + * submits the {@linkplain HttpCommand} associated with {@code invocation}, + * {@link #getTransformer(String, HttpCommand) parses its response}, and + * applies a {@link #getFallback(String, Invocation, HttpCommand) fallback} + * if a {@code Throwable} is encountered. Parsing and Fallback occur on the + * {@code userExecutor} thread. + */ + public ListenableFuture submit(Invocation invocation) { + String commandName = config.getCommandName(invocation); + HttpCommand command = toCommand(commandName, invocation); + Function transformer = getTransformer(commandName, command); + org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); + + logger.debug(">> submitting %s", commandName); + return withFallback(transform(http.submit(command), transformer, userExecutor), fallback); + } + + /** + * invokes the {@linkplain HttpCommand} associated with {@code invocation}, + * {@link #getTransformer(String, HttpCommand) parses its response}, and + * applies a {@link #getFallback(String, Invocation, HttpCommand) fallback} + * if a {@code Throwable} is encountered. + */ + public Object invoke(Invocation invocation) { + String commandName = config.getCommandName(invocation); + HttpCommand command = toCommand(commandName, invocation); + Function transformer = getTransformer(commandName, command); + org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); + + logger.debug(">> invoking %s", commandName); + try { + return transformer.apply(http.invoke(command)); + } catch (Throwable t) { + try { + return fallback.createOrPropagate(t); + } catch (Exception e) { + throw propagate(e); + } + } + } + + /** + * calls {@link #invoke(Invocation)}, timing out after the specified time + * limit. If the target method call finished before the limit is reached, the + * return value or exception is propagated to the caller exactly as-is. If, + * on the other hand, the time limit is reached, we attempt to abort the call + * to the target, and throw an {@link UncheckedTimeoutException} to the + * caller. + * + * @param invocation + * the Invocation to invoke via {@link #invoke(Invocation)} + * @param limitNanos + * with timeoutUnit, the maximum length of time to wait in + * nanoseconds + * @throws InterruptedException + * if our thread is interrupted during execution + * @throws UncheckedTimeoutException + * if the time limit is reached + * @see TimeLimiter#callWithTimeout(Callable, long, TimeUnit, boolean) + */ + public Object invokeWithTimeout(final Invocation invocation, final long limitNanos) { + String commandName = config.getCommandName(invocation); + HttpCommand command = toCommand(commandName, invocation); + org.jclouds.Fallback fallback = getFallback(commandName, invocation, command); + + logger.debug(">> blocking on %s for %s", invocation, limitNanos); + try { + return timeLimiter + .callWithTimeout(new InvokeAndTransform(commandName, command), limitNanos, NANOSECONDS, true); + } catch (Throwable t) { + try { + return fallback.createOrPropagate(t); + } catch (Exception e) { + throw propagate(e); + } + } + } + + private org.jclouds.Fallback getFallback(String commandName, Invocation invocation, HttpCommand command) { + HttpRequest request = command.getCurrentRequest(); + org.jclouds.Fallback fallback = config.getFallback(invocation); + if (fallback instanceof InvocationContext) + InvocationContext.class.cast(fallback).setContext(request); + logger.trace("<< exceptions from %s are parsed by %s", commandName, fallback.getClass().getSimpleName()); + return fallback; + } + + @VisibleForTesting + final class InvokeAndTransform implements Callable { + private final String commandName; + private final HttpCommand command; + private final Function transformer; + + InvokeAndTransform(String commandName, HttpCommand command) { + this.commandName = commandName; + this.command = command; + this.transformer = getTransformer(commandName, command); + } + + @Override + public Object call() throws Exception { + return transformer.apply(http.invoke(command)); + } + + @Override + public int hashCode() { + return Objects.hashCode(commandName, command, transformer); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null || getClass() != obj.getClass()) + return false; + InvokeAndTransform that = InvokeAndTransform.class.cast(obj); + return equal(this.commandName, that.commandName) && equal(this.command, that.command) + && equal(this.transformer, that.transformer); + } + + @Override + public String toString() { + return toStringHelper(this).add("commandName", commandName).add("command", command) + .add("transformer", transformer).toString(); + } + } + + /** + * looks up the corresponding {@code Invocation} that returns a + * {@code Future}. Only Invokables that return {@code Futures} are annotated + * in a way that can be parsed into an {@linkplain HttpRequest}. + */ + private Invocation toAsync(Invocation in) { + Invocation async = sync2async.apply(in); + checkState(isFuture(async.getInvokable()), "not a future: %s", async); + return async; + } + + private HttpCommand toCommand(String commandName, Invocation invocation) { + logger.trace(">> converting %s", commandName); + HttpRequest request = annotationProcessor.apply(invocation); + logger.trace("<< converted %s to %s", commandName, request.getRequestLine()); + return new HttpCommand(request); + } + + private Function getTransformer(String commandName, HttpCommand command) { + HttpRequest request = command.getCurrentRequest(); + Function transformer = transformerForRequest.apply(request); + logger.trace("<< response from %s is parsed by %s", commandName, transformer.getClass().getSimpleName()); + return transformer; + } + + private boolean isFuture(Invokable in) { + return in.getReturnType().getRawType().equals(ListenableFuture.class); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + InvokeMappedHttpMethod that = InvokeMappedHttpMethod.class.cast(o); + return equal(this.annotationProcessor, that.annotationProcessor); + } + + @Override + public int hashCode() { + return Objects.hashCode(annotationProcessor); + } + + @Override + public String toString() { + return Objects.toStringHelper("").omitNullValues().add("annotationParser", annotationProcessor).toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/jclouds/rest/internal/UtilsImpl.java b/core/src/main/java/org/jclouds/rest/internal/UtilsImpl.java index 8ac824667a..5dd2bb1f39 100644 --- a/core/src/main/java/org/jclouds/rest/internal/UtilsImpl.java +++ b/core/src/main/java/org/jclouds/rest/internal/UtilsImpl.java @@ -29,7 +29,6 @@ import org.jclouds.date.DateService; import org.jclouds.domain.Credentials; import org.jclouds.json.Json; import org.jclouds.logging.Logger.LoggerFactory; -import org.jclouds.rest.HttpAsyncClient; import org.jclouds.rest.HttpClient; import org.jclouds.rest.Utils; import org.jclouds.xml.XMLParser; @@ -48,7 +47,7 @@ public class UtilsImpl implements Utils { private final Json json; private final HttpClient simpleClient; - private final HttpAsyncClient simpleAsyncClient; + private final org.jclouds.rest.HttpAsyncClient simpleAsyncClient; private final Crypto encryption; private final DateService date; private final ListeningExecutorService userExecutor; @@ -60,7 +59,7 @@ public class UtilsImpl implements Utils { private XMLParser xml; @Inject - protected UtilsImpl(Injector injector, Json json, XMLParser xml, HttpClient simpleClient, HttpAsyncClient simpleAsyncClient, + protected UtilsImpl(Injector injector, Json json, XMLParser xml, HttpClient simpleClient, org.jclouds.rest.HttpAsyncClient simpleAsyncClient, Crypto encryption, DateService date, @Named(Constants.PROPERTY_USER_THREADS) ListeningExecutorService userExecutor, @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor, EventBus eventBus, Map credentialStore, LoggerFactory loggerFactory) { @@ -79,7 +78,8 @@ public class UtilsImpl implements Utils { } @Override - public HttpAsyncClient asyncHttp() { + @Deprecated + public org.jclouds.rest.HttpAsyncClient asyncHttp() { return simpleAsyncClient; } @@ -104,7 +104,8 @@ public class UtilsImpl implements Utils { } @Override - public HttpAsyncClient getHttpAsyncClient() { + @Deprecated + public org.jclouds.rest.HttpAsyncClient getHttpAsyncClient() { return simpleAsyncClient; } diff --git a/core/src/test/java/org/jclouds/config/BindApiContextWithWildcardExtendsExplicitAndRawTypeTest.java b/core/src/test/java/org/jclouds/config/BindApiContextWithWildcardExtendsExplicitAndRawTypeTest.java new file mode 100644 index 0000000000..ed078a23c3 --- /dev/null +++ b/core/src/test/java/org/jclouds/config/BindApiContextWithWildcardExtendsExplicitAndRawTypeTest.java @@ -0,0 +1,123 @@ +/** + * 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.config; + +import static com.google.common.base.Suppliers.ofInstance; +import static org.easymock.EasyMock.createMock; +import static org.testng.Assert.assertEquals; + +import javax.inject.Inject; + +import org.jclouds.Context; +import org.jclouds.domain.Credentials; +import org.jclouds.http.IntegrationTestAsyncClient; +import org.jclouds.http.IntegrationTestClient; +import org.jclouds.providers.AnonymousProviderMetadata; +import org.jclouds.providers.ProviderMetadata; +import org.jclouds.providers.config.BindProviderMetadataContextAndCredentials; +import org.jclouds.rest.ApiContext; +import org.jclouds.rest.HttpApiMetadata; +import org.jclouds.rest.Utils; +import org.jclouds.rest.internal.BaseRestApiTest.MockModule; +import org.testng.annotations.Test; + +import com.google.common.reflect.TypeToken; +import com.google.inject.AbstractModule; +import com.google.inject.Guice; +import com.google.inject.Injector; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit", testName = "BindApiContextWithWildcardExtendsExplicitAndRawTypeTest") +public class BindApiContextWithWildcardExtendsExplicitAndRawTypeTest { + + @SuppressWarnings("rawtypes") + private static class ExpectedBindings { + + private final ApiContext raw; + private final ApiContext explicit; + + @Inject + public ExpectedBindings(ApiContext raw, ApiContext explicit) { + this.raw = raw; + this.explicit = explicit; + } + + } + + @Test + public void testRawAndExplicit() { + ProviderMetadata md = AnonymousProviderMetadata.forApiOnEndpoint(IntegrationTestClient.class, "http://localhost"); + + ExpectedBindings bindings = injectorFor(md).getInstance(ExpectedBindings.class); + assertEquals(bindings.raw, bindings.explicit); + } + + private Injector injectorFor(ProviderMetadata md) { + return Guice.createInjector(new BindNameToContext("test"), new BindProviderMetadataContextAndCredentials(md, + ofInstance(new Credentials("user", "pass"))), new BindApiContextWithWildcardExtendsExplicitAndRawType( + HttpApiMetadata.class.cast(md.getApiMetadata())), + + // stuff needed for ApiContextImpl + new MockModule(), new AbstractModule() { + + @Override + protected void configure() { + bind(Utils.class).toInstance(createMock(Utils.class)); + bind(IntegrationTestClient.class).toInstance(createMock(IntegrationTestClient.class)); + bind(IntegrationTestAsyncClient.class).toInstance(createMock(IntegrationTestAsyncClient.class)); + } + }); + } + + @SuppressWarnings("rawtypes") + private static class ExpectedBindingsWithWildCardExtends { + + private final ApiContext raw; + private final ApiContext explicit; + private final ApiContext wildcardExtends; + + @Inject + public ExpectedBindingsWithWildCardExtends(ApiContext raw, ApiContext explicit, + ApiContext wildcardExtends) { + this.raw = raw; + this.explicit = explicit; + this.wildcardExtends = wildcardExtends; + } + + } + + @Test + public void testRawExplicitAndWildCardExtends() { + ProviderMetadata md = AnonymousProviderMetadata.forApiOnEndpoint(IntegrationTestClient.class, "http://localhost"); + + TypeToken wildCardExtendsType = new TypeToken>() { + private static final long serialVersionUID = 1L; + }; + + md = md.toBuilder().apiMetadata(md.getApiMetadata().toBuilder().context(wildCardExtendsType).build()).build(); + + ExpectedBindingsWithWildCardExtends bindings = injectorFor(md).getInstance( + ExpectedBindingsWithWildCardExtends.class); + assertEquals(bindings.raw, bindings.explicit); + assertEquals(bindings.explicit, bindings.wildcardExtends); + } +} diff --git a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java index 822abb12de..f1119637fc 100644 --- a/core/src/test/java/org/jclouds/http/IntegrationTestClient.java +++ b/core/src/test/java/org/jclouds/http/IntegrationTestClient.java @@ -18,12 +18,42 @@ */ package org.jclouds.http; -import java.io.Closeable; +import static com.google.common.util.concurrent.Futures.immediateFuture; +import java.io.Closeable; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Singleton; +import javax.ws.rs.GET; +import javax.ws.rs.HEAD; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.HttpMethod; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +import org.jclouds.Fallbacks.FalseOnNotFoundOr404; +import org.jclouds.http.functions.ParseSax; import org.jclouds.http.options.HttpRequestOptions; import org.jclouds.io.Payload; +import org.jclouds.rest.annotations.BinderParam; +import org.jclouds.rest.annotations.Fallback; +import org.jclouds.rest.annotations.MapBinder; +import org.jclouds.rest.annotations.PayloadParam; +import org.jclouds.rest.annotations.RequestFilters; +import org.jclouds.rest.annotations.ResponseParser; +import org.jclouds.rest.annotations.XMLResponseParser; +import org.jclouds.rest.binders.BindToJsonPayload; +import org.jclouds.rest.binders.BindToStringPayload; +import org.jclouds.util.Strings2; +import com.google.common.base.Function; import com.google.common.collect.Multimap; +import com.google.common.util.concurrent.ListenableFuture; import com.google.inject.Provides; /** @@ -32,37 +62,144 @@ import com.google.inject.Provides; * @author Adrian Cole */ public interface IntegrationTestClient extends Closeable { - String rowdy(String path); + @Target({ ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @HttpMethod("ROWDY") + public @interface ROWDY { + } - boolean exists(String path); + @ROWDY + @Path("/objects/{id}") + String rowdy(@PathParam("id") String path); - String synch(String id); + @HEAD + @Path("/objects/{id}") + @Fallback(FalseOnNotFoundOr404.class) + boolean exists(@PathParam("id") String path); - String download(String id); + @GET + @Path("/objects/{id}") + String download(@PathParam("id") String id); HttpResponse invoke(HttpRequest request); + + @GET + @Path("/{path}") + String synch(@PathParam("path") String id); - String downloadException(String id, HttpRequestOptions options); + @GET + @Path("/objects/{id}") + @Fallback(FooOnException.class) + String downloadException(@PathParam("id") String id, HttpRequestOptions options); - String synchException(String id, String header); + static class FooOnException implements org.jclouds.Fallback { + public ListenableFuture create(Throwable t) throws Exception { + return immediateFuture("foo"); + } - String upload(String id, String toPut); + public String createOrPropagate(Throwable t) throws Exception { + return "foo"; + } + } - String post(String id, String toPut); + @GET + @Path("/objects/{id}") + @Fallback(FooOnException.class) + String synchException(@PathParam("id") String id, @HeaderParam("Range") String header); - String postAsInputStream(String id, String toPut); + @PUT + @Path("/objects/{id}") + String upload(@PathParam("id") String id, @BinderParam(BindToStringPayload.class) String toPut); - Multimap postPayloadAndReturnHeaders(String id, Payload payload); + @POST + @Path("/objects/{id}") + String post(@PathParam("id") String id, @BinderParam(BindToStringPayload.class) String toPut); - String postJson(String id, String toPut); + @POST + @Path("/objects/{id}") + String postAsInputStream(@PathParam("id") String id, + @BinderParam(BindToInputStreamPayload.class) String toPut); - String downloadFilter(String id, String header); + static class BindToInputStreamPayload extends BindToStringPayload { + @Override + public R bindToRequest(R request, Object payload) { + request.setPayload(Strings2.toInputStream(payload.toString())); + request.getPayload().getContentMetadata().setContentLength((long) payload.toString().getBytes().length); + return request; + } + } - String download(String id, String header); + @Singleton + static class ResponsePayload implements Function> { - String downloadAndParse(String id); + public Multimap apply(HttpResponse from) { + return from.getHeaders(); + } - void putNothing(String id); + } + + @POST + @Path("/objects/{id}") + @ResponseParser(ResponsePayload.class) + Multimap postPayloadAndReturnHeaders(@PathParam("id") String id, Payload payload); + + @POST + @Path("/objects/{id}") + @MapBinder(BindToJsonPayload.class) + String postJson(@PathParam("id") String id, @PayloadParam("key") String toPut); + + @GET + @Path("/objects/{id}") + @RequestFilters(Filter.class) + String downloadFilter(@PathParam("id") String id, @HeaderParam("filterme") String header); + + static class Filter implements HttpRequestFilter { + public HttpRequest filter(HttpRequest request) throws HttpException { + if (request.getHeaders().containsKey("filterme")) { + request = request.toBuilder().replaceHeader("test", "test").build(); + } + return request; + } + } + + @GET + @Path("/objects/{id}") + String download(@PathParam("id") String id, @HeaderParam("test") String header); + + @GET + @Path("/objects/{id}") + @XMLResponseParser(BarHandler.class) + String downloadAndParse(@PathParam("id") String id); + + public static class BarHandler extends ParseSax.HandlerWithResult { + + private String bar = null; + private StringBuilder currentText = new StringBuilder(); + + @Override + public void endElement(String uri, String name, String qName) { + if (qName.equals("bar")) { + bar = currentText.toString(); + } + currentText = new StringBuilder(); + } + + @Override + public void characters(char ch[], int start, int length) { + currentText.append(ch, start, length); + + } + + @Override + public String getResult() { + return bar; + } + + } + + @PUT + @Path("/objects/{id}") + void putNothing(@PathParam("id") String id); @Provides StringBuilder newStringBuilder(); diff --git a/core/src/test/java/org/jclouds/rest/config/RestModuleTest.java b/core/src/test/java/org/jclouds/rest/config/MappedHttpInvocationModuleTest.java similarity index 88% rename from core/src/test/java/org/jclouds/rest/config/RestModuleTest.java rename to core/src/test/java/org/jclouds/rest/config/MappedHttpInvocationModuleTest.java index 0a48f1e8c1..1d36ab2ed5 100644 --- a/core/src/test/java/org/jclouds/rest/config/RestModuleTest.java +++ b/core/src/test/java/org/jclouds/rest/config/MappedHttpInvocationModuleTest.java @@ -43,7 +43,7 @@ import com.google.common.util.concurrent.ListenableFuture; * @author Adrian Cole */ @Test(groups = "unit") -public class RestModuleTest { +public class MappedHttpInvocationModuleTest { static interface Sync { String get(); } @@ -54,7 +54,7 @@ public class RestModuleTest { public void testPutInvokablesWhenInterfacesMatch() { Cache, Invokable> cache = CacheBuilder.newBuilder().build(); - RestModule.putInvokables(Sync.class, Async.class, cache); + MappedHttpInvocationModule.putInvokables(Sync.class, Async.class, cache); assertEquals(cache.size(), 1); @@ -78,7 +78,7 @@ public class RestModuleTest { @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = ".* has different typed exceptions than target .*") public void testPutInvokablesWhenInterfacesMatchExceptExceptions() { Cache, Invokable> cache = CacheBuilder.newBuilder().build(); - RestModule.putInvokables(Sync.class, AsyncWithException.class, cache); + MappedHttpInvocationModule.putInvokables(Sync.class, AsyncWithException.class, cache); } private static interface AsyncWithMisnamedMethod { @@ -88,7 +88,7 @@ public class RestModuleTest { @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = "no such method .*") public void testPutInvokablesWhenTargetMethodNotFound() { Cache, Invokable> cache = CacheBuilder.newBuilder().build(); - RestModule.putInvokables(Sync.class, AsyncWithMisnamedMethod.class, cache); + MappedHttpInvocationModule.putInvokables(Sync.class, AsyncWithMisnamedMethod.class, cache); } static final Predicate, Invokable>> isHttpInvokable = new Predicate, Invokable>>() { @@ -99,7 +99,7 @@ public class RestModuleTest { }; public void testSeedKnownSync2AsyncIncludesHttpClientByDefault() { - Map, Invokable> cache = RestModule.seedKnownSync2AsyncInvokables( + Map, Invokable> cache = MappedHttpInvocationModule.seedKnownSync2AsyncInvokables( ImmutableMap., Class> of()).asMap(); assertEquals(cache.size(), 6); @@ -107,7 +107,7 @@ public class RestModuleTest { } public void testSeedKnownSync2AsyncInvokablesInterfacesMatch() { - Map, Invokable> cache = RestModule.seedKnownSync2AsyncInvokables( + Map, Invokable> cache = MappedHttpInvocationModule.seedKnownSync2AsyncInvokables( ImmutableMap., Class> of(Sync.class, Async.class)).asMap(); assertEquals(cache.size(), 7); diff --git a/core/src/test/java/org/jclouds/rest/internal/BaseHttpApiMetadataTest.java b/core/src/test/java/org/jclouds/rest/internal/BaseHttpApiMetadataTest.java new file mode 100644 index 0000000000..2a588673be --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/internal/BaseHttpApiMetadataTest.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.rest.internal; +import static org.jclouds.reflect.Reflection2.typeToken; + +import java.util.Set; + +import org.jclouds.View; +import org.jclouds.apis.ApiMetadata; +import org.jclouds.apis.Apis; +import org.jclouds.apis.internal.BaseApiMetadataTest; +import org.jclouds.rest.ApiContext; +import org.jclouds.rest.HttpApiMetadata; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.TypeToken; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit") +public abstract class BaseHttpApiMetadataTest extends BaseApiMetadataTest { + + public BaseHttpApiMetadataTest(HttpApiMetadata toTest, Set> views) { + super(toTest, views); + } + + @Test + public void testContextAssignableFromRestContext() { + Set all = ImmutableSet.copyOf(Apis.contextAssignableFrom(typeToken(ApiContext.class))); + assert all.contains(toTest) : String.format("%s not found in %s", toTest, all); + } + +} diff --git a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java index 4154bab67e..b857ced9fa 100644 --- a/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/BaseRestApiExpectTest.java @@ -70,7 +70,7 @@ import org.jclouds.io.Payload; import org.jclouds.io.Payloads; import org.jclouds.logging.config.NullLoggingModule; import org.jclouds.providers.ProviderMetadata; -import org.jclouds.rest.RestApiMetadata; +import org.jclouds.rest.HttpApiMetadata; import org.jclouds.rest.config.CredentialStoreModule; import org.jclouds.util.Strings2; import org.w3c.dom.Node; @@ -557,11 +557,16 @@ public abstract class BaseRestApiExpectTest { ApiMetadata am = (pm != null) ? pm.getApiMetadata() : checkNotNull(createApiMetadata(), "either createApiMetadata or createProviderMetadata must be overridden"); - builder = pm != null ? ContextBuilder.newBuilder(pm) : ContextBuilder.newBuilder(RestApiMetadata.class.cast(am)); + builder = pm != null ? ContextBuilder.newBuilder(pm) : ContextBuilder.newBuilder(am); + } + ApiMetadata am = builder.getApiMetadata(); + if (am instanceof HttpApiMetadata) { + this.api = HttpApiMetadata.class.cast(am).getApi(); + } else if (am instanceof org.jclouds.rest.RestApiMetadata) { + this.api = org.jclouds.rest.RestApiMetadata.class.cast(am).getApi(); + } else { + throw new UnsupportedOperationException("unsupported base type: " + am); } - - this.api = RestApiMetadata.class.cast(builder.getApiMetadata()).getApi(); - // isolate tests from eachother, as default credentialStore is static return builder.credentials(identity, credential).modules( ImmutableSet.of(new ExpectModule(fn), new NullLoggingModule(), new CredentialStoreModule(new CopyInputStreamInputSupplierMap( diff --git a/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java b/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java index 71dade0220..dc9f5eca2f 100644 --- a/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java +++ b/core/src/test/java/org/jclouds/rest/internal/InvokeHttpMethodTest.java @@ -18,9 +18,7 @@ */ package org.jclouds.rest.internal; -import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.eq; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; @@ -48,10 +46,7 @@ import com.google.common.base.Functions; import com.google.common.base.Optional; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.TimeLimiter; /** @@ -62,17 +57,11 @@ import com.google.common.util.concurrent.TimeLimiter; public class InvokeHttpMethodTest { public static interface ThingApi { + @Named("ns:get") HttpResponse get(); } - public static interface ThingAsyncApi { - @Named("ns:get") - ListenableFuture get(); - } - private Invocation get; - private Invocation asyncGet; - private Function sync2async; private HttpRequest getRequest = HttpRequest.builder().method("GET").endpoint("http://get").build(); private HttpCommand getCommand = new HttpCommand(getRequest); private Function toRequest; @@ -80,15 +69,12 @@ public class InvokeHttpMethodTest { @BeforeClass void setupInvocations() throws SecurityException, NoSuchMethodException { get = Invocation.create(method(ThingApi.class, "get"), ImmutableList.of()); - asyncGet = Invocation.create(method(ThingAsyncApi.class, "get"), ImmutableList.of()); - sync2async = Functions.forMap(ImmutableMap.of(get, asyncGet)); - toRequest = Functions.forMap(ImmutableMap.of(asyncGet, getRequest)); + toRequest = Functions.forMap(ImmutableMap.of(get, getRequest)); } @SuppressWarnings("unchecked") private Function> transformerForRequest = Function.class.cast(Functions .constant(Functions.identity())); - private ListeningExecutorService userThreads = MoreExecutors.sameThreadExecutor(); private HttpResponse response = HttpResponse.builder().statusCode(200).payload("foo").build(); private HttpCommandExecutorService http; @@ -108,10 +94,9 @@ public class InvokeHttpMethodTest { fallback = createMock(org.jclouds.Fallback.class); config = createMock(InvocationConfig.class); future = createMock(ListenableFuture.class); - invokeHttpMethod = new InvokeHttpMethod(sync2async, toRequest, http, transformerForRequest, timeLimiter, config, - userThreads); - expect(config.getCommandName(asyncGet)).andReturn("ns:get"); - expect(config.getFallback(asyncGet)).andReturn(fallback); + invokeHttpMethod = new InvokeHttpMethod(toRequest, http, transformerForRequest, timeLimiter, config); + expect(config.getCommandName(get)).andReturn("ns:get"); + expect(config.getFallback(get)).andReturn(fallback); } @AfterMethod @@ -120,7 +105,7 @@ public class InvokeHttpMethodTest { } public void testMethodWithTimeoutRunsTimeLimiter() throws Exception { - expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional.of(250000000l)); + expect(config.getTimeoutNanos(get)).andReturn(Optional.of(250000000l)); InvokeAndTransform invoke = invokeHttpMethod.new InvokeAndTransform("ns:get", getCommand); expect(timeLimiter.callWithTimeout(invoke, 250000000, TimeUnit.NANOSECONDS, true)).andReturn(response); replay(http, timeLimiter, fallback, config, future); @@ -128,24 +113,17 @@ public class InvokeHttpMethodTest { } public void testMethodWithNoTimeoutCallGetDirectly() throws Exception { - expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional. absent()); + expect(config.getTimeoutNanos(get)).andReturn(Optional. absent()); expect(http.invoke(new HttpCommand(getRequest))).andReturn(response); replay(http, timeLimiter, fallback, config, future); invokeHttpMethod.apply(get); } - public void testAsyncMethodSubmitsRequest() throws Exception { - expect(http.submit(new HttpCommand(getRequest))).andReturn(future); - future.addListener(anyObject(Runnable.class), eq(userThreads)); - replay(http, timeLimiter, fallback, config, future); - invokeHttpMethod.apply(asyncGet); - } - private HttpResponse fallbackResponse = HttpResponse.builder().statusCode(200).payload("bar").build(); public void testDirectCallRunsFallbackCreateOrPropagate() throws Exception { IllegalStateException exception = new IllegalStateException(); - expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional. absent()); + expect(config.getTimeoutNanos(get)).andReturn(Optional. absent()); expect(http.invoke(new HttpCommand(getRequest))).andThrow(exception); expect(fallback.createOrPropagate(exception)).andReturn(fallbackResponse); replay(http, timeLimiter, fallback, config, future); @@ -154,24 +132,11 @@ public class InvokeHttpMethodTest { public void testTimeLimitedRunsFallbackCreateOrPropagate() throws Exception { IllegalStateException exception = new IllegalStateException(); - expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional.of(250000000l)); + expect(config.getTimeoutNanos(get)).andReturn(Optional.of(250000000l)); InvokeAndTransform invoke = invokeHttpMethod.new InvokeAndTransform("ns:get", getCommand); expect(timeLimiter.callWithTimeout(invoke, 250000000, TimeUnit.NANOSECONDS, true)).andThrow(exception); expect(fallback.createOrPropagate(exception)).andReturn(fallbackResponse); replay(http, timeLimiter, fallback, config, future); assertEquals(invokeHttpMethod.apply(get), fallbackResponse); } - - @SuppressWarnings("unchecked") - public void testSubmitRunsFallbackCreateOnGet() throws Exception { - IllegalStateException exception = new IllegalStateException(); - expect(http.submit(new HttpCommand(getRequest))).andReturn( - Futures. immediateFailedFuture(exception)); - expect(fallback.create(exception)).andReturn(Futures. immediateFuture(fallbackResponse)); - // not using the field, as you can see above we are making an immediate - // failed future instead. - future = createMock(ListenableFuture.class); - replay(http, timeLimiter, fallback, config, future); - assertEquals(ListenableFuture.class.cast(invokeHttpMethod.apply(asyncGet)).get(), fallbackResponse); - } } diff --git a/core/src/test/java/org/jclouds/rest/internal/InvokeMappedHttpMethodTest.java b/core/src/test/java/org/jclouds/rest/internal/InvokeMappedHttpMethodTest.java new file mode 100644 index 0000000000..a4023dd5d3 --- /dev/null +++ b/core/src/test/java/org/jclouds/rest/internal/InvokeMappedHttpMethodTest.java @@ -0,0 +1,177 @@ +/** + * 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.rest.internal; + +import static org.easymock.EasyMock.anyObject; +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.eq; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.jclouds.reflect.Reflection2.method; +import static org.testng.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; + +import javax.inject.Named; + +import org.jclouds.http.HttpCommand; +import org.jclouds.http.HttpCommandExecutorService; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.reflect.Invocation; +import org.jclouds.rest.config.InvocationConfig; +import org.jclouds.rest.internal.InvokeMappedHttpMethod.InvokeAndTransform; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.base.Function; +import com.google.common.base.Functions; +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.TimeLimiter; + +/** + * + * @author Adrian Cole + */ +@Test(groups = "unit", singleThreaded = true) +public class InvokeMappedHttpMethodTest { + + public static interface ThingApi { + HttpResponse get(); + } + + public static interface ThingAsyncApi { + @Named("ns:get") + ListenableFuture get(); + } + + private Invocation get; + private Invocation asyncGet; + private Function sync2async; + private HttpRequest getRequest = HttpRequest.builder().method("GET").endpoint("http://get").build(); + private HttpCommand getCommand = new HttpCommand(getRequest); + private Function toRequest; + + @BeforeClass + void setupInvocations() throws SecurityException, NoSuchMethodException { + get = Invocation.create(method(ThingApi.class, "get"), ImmutableList.of()); + asyncGet = Invocation.create(method(ThingAsyncApi.class, "get"), ImmutableList.of()); + sync2async = Functions.forMap(ImmutableMap.of(get, asyncGet)); + toRequest = Functions.forMap(ImmutableMap.of(asyncGet, getRequest)); + } + + @SuppressWarnings("unchecked") + private Function> transformerForRequest = Function.class.cast(Functions + .constant(Functions.identity())); + private ListeningExecutorService userThreads = MoreExecutors.sameThreadExecutor(); + + private HttpResponse response = HttpResponse.builder().statusCode(200).payload("foo").build(); + private HttpCommandExecutorService http; + private TimeLimiter timeLimiter; + @SuppressWarnings("rawtypes") + private org.jclouds.Fallback fallback; + private InvocationConfig config; + private InvokeMappedHttpMethod invokeHttpMethod; + + private ListenableFuture future; + + @SuppressWarnings("unchecked") + @BeforeMethod + void createMocks() { + http = createMock(HttpCommandExecutorService.class); + timeLimiter = createMock(TimeLimiter.class); + fallback = createMock(org.jclouds.Fallback.class); + config = createMock(InvocationConfig.class); + future = createMock(ListenableFuture.class); + invokeHttpMethod = new InvokeMappedHttpMethod(sync2async, toRequest, http, transformerForRequest, timeLimiter, config, + userThreads); + expect(config.getCommandName(asyncGet)).andReturn("ns:get"); + expect(config.getFallback(asyncGet)).andReturn(fallback); + } + + @AfterMethod + void verifyMocks() { + verify(http, timeLimiter, fallback, config, future); + } + + public void testMethodWithTimeoutRunsTimeLimiter() throws Exception { + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional.of(250000000l)); + InvokeAndTransform invoke = invokeHttpMethod.new InvokeAndTransform("ns:get", getCommand); + expect(timeLimiter.callWithTimeout(invoke, 250000000, TimeUnit.NANOSECONDS, true)).andReturn(response); + replay(http, timeLimiter, fallback, config, future); + invokeHttpMethod.apply(get); + } + + public void testMethodWithNoTimeoutCallGetDirectly() throws Exception { + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional. absent()); + expect(http.invoke(new HttpCommand(getRequest))).andReturn(response); + replay(http, timeLimiter, fallback, config, future); + invokeHttpMethod.apply(get); + } + + public void testAsyncMethodSubmitsRequest() throws Exception { + expect(http.submit(new HttpCommand(getRequest))).andReturn(future); + future.addListener(anyObject(Runnable.class), eq(userThreads)); + replay(http, timeLimiter, fallback, config, future); + invokeHttpMethod.apply(asyncGet); + } + + private HttpResponse fallbackResponse = HttpResponse.builder().statusCode(200).payload("bar").build(); + + public void testDirectCallRunsFallbackCreateOrPropagate() throws Exception { + IllegalStateException exception = new IllegalStateException(); + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional. absent()); + expect(http.invoke(new HttpCommand(getRequest))).andThrow(exception); + expect(fallback.createOrPropagate(exception)).andReturn(fallbackResponse); + replay(http, timeLimiter, fallback, config, future); + assertEquals(invokeHttpMethod.apply(get), fallbackResponse); + } + + public void testTimeLimitedRunsFallbackCreateOrPropagate() throws Exception { + IllegalStateException exception = new IllegalStateException(); + expect(config.getTimeoutNanos(asyncGet)).andReturn(Optional.of(250000000l)); + InvokeAndTransform invoke = invokeHttpMethod.new InvokeAndTransform("ns:get", getCommand); + expect(timeLimiter.callWithTimeout(invoke, 250000000, TimeUnit.NANOSECONDS, true)).andThrow(exception); + expect(fallback.createOrPropagate(exception)).andReturn(fallbackResponse); + replay(http, timeLimiter, fallback, config, future); + assertEquals(invokeHttpMethod.apply(get), fallbackResponse); + } + + @SuppressWarnings("unchecked") + public void testSubmitRunsFallbackCreateOnGet() throws Exception { + IllegalStateException exception = new IllegalStateException(); + expect(http.submit(new HttpCommand(getRequest))).andReturn( + Futures. immediateFailedFuture(exception)); + expect(fallback.create(exception)).andReturn(Futures. immediateFuture(fallbackResponse)); + // not using the field, as you can see above we are making an immediate + // failed future instead. + future = createMock(ListenableFuture.class); + replay(http, timeLimiter, fallback, config, future); + assertEquals(ListenableFuture.class.cast(invokeHttpMethod.apply(asyncGet)).get(), fallbackResponse); + } +} diff --git a/providers/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java b/providers/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java index dbfda6b6be..a56b23dcbc 100644 --- a/providers/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java +++ b/providers/dynect/src/main/java/org/jclouds/dynect/v3/config/DynECTRestClientModule.java @@ -19,7 +19,7 @@ package org.jclouds.dynect.v3.config; import static org.jclouds.http.HttpUtils.closeClientButKeepContentStream; -import static org.jclouds.rest.config.BinderUtils.bindHttpApi; +import static org.jclouds.rest.config.BinderUtils.bindMappedHttpApi; import java.io.IOException; import java.net.HttpURLConnection; @@ -106,7 +106,7 @@ public class DynECTRestClientModule extends RestClientModule