blob: 7c6cbc599132bb664e4e920ed03e631ad4ba8df4 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds;
import static com.google.common.base.MoreObjects.toStringHelper;
import static com.google.common.base.Preconditions.checkNotNull;
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.Throwables.propagate;
import static com.google.common.collect.Iterables.addAll;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.filter;
import static com.google.common.collect.Iterables.find;
import static com.google.common.collect.Iterables.transform;
import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Lists.newArrayListWithCapacity;
import static com.google.common.collect.Maps.filterKeys;
import static com.google.common.util.concurrent.MoreExecutors.newDirectExecutorService;
import static org.jclouds.Constants.PROPERTY_API;
import static org.jclouds.Constants.PROPERTY_API_VERSION;
import static org.jclouds.Constants.PROPERTY_BUILD_VERSION;
import static org.jclouds.Constants.PROPERTY_CREDENTIAL;
import static org.jclouds.Constants.PROPERTY_ENDPOINT;
import static org.jclouds.Constants.PROPERTY_IDENTITY;
import static org.jclouds.Constants.PROPERTY_ISO3166_CODES;
import static org.jclouds.Constants.PROPERTY_PROVIDER;
import static org.jclouds.reflect.Reflection2.typeToken;
import static org.jclouds.rest.config.BinderUtils.bindHttpApi;
import static org.jclouds.util.Throwables2.propagateAuthorizationOrOriginalException;
import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import org.jclouds.apis.ApiMetadata;
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.domain.Credentials;
import org.jclouds.events.config.ConfiguresEventBus;
import org.jclouds.events.config.EventBusModule;
import org.jclouds.functions.ExpandProperties;
import org.jclouds.http.config.ConfiguresHttpCommandExecutorService;
import org.jclouds.http.config.JavaUrlHttpCommandExecutorServiceModule;
import org.jclouds.javax.annotation.Nullable;
import org.jclouds.lifecycle.config.LifeCycleModule;
import org.jclouds.logging.config.LoggingModule;
import org.jclouds.logging.jdk.config.JDKLoggingModule;
import org.jclouds.providers.ProviderMetadata;
import org.jclouds.providers.Providers;
import org.jclouds.providers.config.BindProviderMetadataContextAndCredentials;
import org.jclouds.providers.internal.UpdateProviderMetadataFromProperties;
import org.jclouds.reflect.Invocation;
import org.jclouds.rest.ConfiguresCredentialStore;
import org.jclouds.rest.ConfiguresHttpApi;
import org.jclouds.rest.HttpApiMetadata;
import org.jclouds.rest.HttpClient;
import org.jclouds.rest.config.CredentialStoreModule;
import org.jclouds.rest.config.HttpApiModule;
import org.jclouds.rest.config.RestModule;
import org.jclouds.rest.internal.InvokeHttpMethod;
import org.jclouds.util.TypeTokenUtils;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableMultimap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.reflect.TypeToken;
import com.google.common.util.concurrent.ExecutionList;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Stage;
import com.google.inject.TypeLiteral;
/**
* Creates {@link Context} or {@link Injector} configured to an api and
* endpoint. Alternatively, this can be used to make a portable {@link View} of
* that api.
*
* <br/>
* ex. to build a {@code Api} on a particular endpoint using the typed
* interface
*
* <pre>
* api = ContextBuilder.newBuilder(new NovaApiMetadata())
* .endpoint("http://10.10.10.10:5000/v2.0")
* .credentials(user, pass)
* .buildApi(NovaApi.class);
* </pre>
*
* <br/>
* ex. to build a {@link View} of a particular backend context, looked up by
* key.
*
* <pre>
* context = ContextBuilder.newBuilder("aws-s3")
* .credentials(apikey, secret)
* .buildView(BlobStoreContext.class);
* </pre>
*
* <h4>Assumptions</h4>
*
* Threadsafe objects will be bound as singletons to the Injector or Context
* provided.
* <p/>
* If no <code>Module</code>s are specified, the default
* {@link JDKLoggingModule logging} and
* {@link JavaUrlHttpCommandExecutorServiceModule http transports} will be
* installed.
*
* @see Context
* @see View
* @see ApiMetadata
* @see ProviderMetadata
*/
public class ContextBuilder {
private static final Stage GUICE_STAGE = Stage.PRODUCTION;
/**
* looks up a provider or api with the given id
*
* @param providerOrApi
* id of the provider or api
* @return means to build a context to that provider
* @throws NoSuchElementException
* if the id was not configured.
*/
public static ContextBuilder newBuilder(String providerOrApi) throws NoSuchElementException {
try {
try {
return ContextBuilder.newBuilder(Providers.withId(providerOrApi));
} catch (NoSuchElementException e) {
return ContextBuilder.newBuilder(Apis.withId(providerOrApi));
}
} catch (NoSuchElementException e) {
Builder<String, String> builder = ImmutableMultimap.<String, String> builder();
builder.putAll("providers", transform(Providers.all(), Providers.idFunction()));
builder.putAll("apis", transform(Apis.all(), Apis.idFunction()));
throw new NoSuchElementException(String.format("key [%s] not in the list of providers or apis: %s",
providerOrApi, builder.build()));
}
}
public static ContextBuilder newBuilder(ApiMetadata apiMetadata) {
try {
return new ContextBuilder(apiMetadata);
} catch (Exception e) {
return propagateAuthorizationOrOriginalException(e);
}
}
public static ContextBuilder newBuilder(ProviderMetadata providerMetadata) {
try {
return new ContextBuilder(providerMetadata);
} catch (Exception e) {
return propagateAuthorizationOrOriginalException(e);
}
}
protected Optional<String> name = Optional.absent();
protected Optional<ProviderMetadata> providerMetadata = Optional.absent();
protected final String providerId;
protected Optional<String> endpoint = Optional.absent();
protected Optional<String> identity = Optional.absent();
protected Optional<Supplier<Credentials>> credentialsSupplierOption = Optional.absent();
@Nullable
protected String credential;
protected ApiMetadata apiMetadata;
protected String apiVersion;
protected String buildVersion;
protected Optional<Properties> overrides = Optional.absent();
protected List<Module> modules = newArrayListWithCapacity(3);
@Override
public String toString() {
return toStringHelper("").add("providerMetadata", providerMetadata).add("apiMetadata", apiMetadata).toString();
}
protected ContextBuilder(ProviderMetadata providerMetadata) {
this(providerMetadata, checkNotNull(providerMetadata, "providerMetadata").getApiMetadata());
}
protected ContextBuilder(@Nullable ProviderMetadata providerMetadata, ApiMetadata apiMetadata) {
this.apiMetadata = checkNotNull(apiMetadata, "apiMetadata");
if (providerMetadata != null) {
this.providerMetadata = Optional.of(providerMetadata);
this.endpoint = Optional.of(providerMetadata.getEndpoint());
this.providerId = providerMetadata.getId();
} else {
this.endpoint = apiMetadata.getDefaultEndpoint();
this.providerId = apiMetadata.getId();
}
this.identity = apiMetadata.getDefaultIdentity();
this.credential = apiMetadata.getDefaultCredential().orNull();
this.apiVersion = apiMetadata.getVersion();
this.buildVersion = apiMetadata.getBuildVersion().or("");
}
public ContextBuilder(ApiMetadata apiMetadata) {
this(null, apiMetadata);
}
public ContextBuilder name(String name) {
this.name = Optional.of(checkNotNull(name, "name"));
return this;
}
/**
* returns the current login credentials. jclouds will not cache this value. Use this when you need to change
* credentials at runtime.
*/
public ContextBuilder credentialsSupplier(Supplier<Credentials> credentialsSupplier) {
this.credentialsSupplierOption = Optional.of(checkNotNull(credentialsSupplier, "credentialsSupplier"));
return this;
}
/**
* constant value of the cloud identity and credential.
*
* @param credential (optional depending on {@link ApiMetadata#getCredentialName()}
*/
public ContextBuilder credentials(String identity, @Nullable String credential) {
this.identity = Optional.of(checkNotNull(identity, "identity"));
this.credential = credential;
return this;
}
public ContextBuilder endpoint(String endpoint) {
this.endpoint = Optional.of(checkNotNull(endpoint, "endpoint"));
return this;
}
public ContextBuilder apiVersion(String apiVersion) {
this.apiVersion = checkNotNull(apiVersion, "apiVersion");
return this;
}
public ContextBuilder buildVersion(String buildVersion) {
this.buildVersion = checkNotNull(buildVersion, "buildVersion");
return this;
}
public ContextBuilder modules(Iterable<? extends Module> modules) {
addAll(this.modules, checkNotNull(modules, "modules"));
return this;
}
public ContextBuilder overrides(Properties overrides) {
this.overrides = Optional.of(checkNotNull(overrides, "overrides"));
return this;
}
public static String searchPropertiesForProviderScopedProperty(Properties mutable, String prov, String key) throws NoSuchElementException {
try {
return find(newArrayList(mutable.getProperty(prov + "." + key), mutable.getProperty("jclouds." + key)),
notNull());
} catch (NoSuchElementException e) {
throw new NoSuchElementException(String.format("property %s.%s not present in properties: %s", prov, key, mutable.keySet()));
} finally {
mutable.remove(prov + "." + key);
mutable.remove("jclouds." + key);
}
}
public Injector buildInjector() {
Properties unexpanded = currentStateToUnexpandedProperties();
Set<String> keysToResolve = ImmutableSet.of(PROPERTY_IDENTITY, PROPERTY_CREDENTIAL, PROPERTY_ENDPOINT,
PROPERTY_API, PROPERTY_API_VERSION, PROPERTY_BUILD_VERSION);
Set<String> optionalKeys;
if (credentialsSupplierOption.isPresent()) {
optionalKeys = ImmutableSet.of(PROPERTY_IDENTITY, PROPERTY_CREDENTIAL);
} else if (!apiMetadata.getCredentialName().isPresent()) {
optionalKeys = ImmutableSet.of(PROPERTY_CREDENTIAL);
} else {
optionalKeys = ImmutableSet.of();
}
Properties resolved = resolveProperties(unexpanded, providerId, keysToResolve, optionalKeys);
Properties expanded = new ExpandProperties().apply(resolved);
Supplier<Credentials> credentialsSupplier = buildCredentialsSupplier(expanded);
ProviderMetadata providerMetadata = new UpdateProviderMetadataFromProperties(apiMetadata, this.providerMetadata)
.apply(expanded);
// We use either the specified name (optional) or a hash of provider/api, endpoint, api version & identity. Hash
// is used to be something readable.
return buildInjector(name.or(String.valueOf(Objects.hashCode(providerMetadata.getId(),
providerMetadata.getEndpoint(), providerMetadata.getApiMetadata().getVersion(), credentialsSupplier))),
providerMetadata, credentialsSupplier, modules);
}
protected Supplier<Credentials> buildCredentialsSupplier(Properties expanded) {
Credentials creds = new Credentials(getAndRemove(expanded, PROPERTY_IDENTITY), getAndRemove(expanded,
PROPERTY_CREDENTIAL));
Supplier<Credentials> credentialsSupplier;
if (credentialsSupplierOption.isPresent()) {
credentialsSupplier = credentialsSupplierOption.get();
} else {
credentialsSupplier = Suppliers.ofInstance(creds);
}
return credentialsSupplier;
}
private static String getAndRemove(Properties expanded, String key) {
try {
return expanded.getProperty(key);
} finally {
expanded.remove(key);
}
}
private Properties currentStateToUnexpandedProperties() {
Properties defaults = new Properties();
putAllAsString(apiMetadata.getDefaultProperties(), defaults);
defaults.setProperty(PROPERTY_PROVIDER, providerId);
if (providerMetadata.isPresent()) {
putAllAsString(providerMetadata.get().getDefaultProperties(), defaults);
defaults.setProperty(PROPERTY_ISO3166_CODES, Joiner.on(',').join(providerMetadata.get().getIso3166Codes()));
}
if (endpoint.isPresent())
defaults.setProperty(PROPERTY_ENDPOINT, endpoint.get());
defaults.setProperty(PROPERTY_API, apiMetadata.getName());
defaults.setProperty(PROPERTY_API_VERSION, apiVersion);
defaults.setProperty(PROPERTY_BUILD_VERSION, buildVersion);
if (identity.isPresent())
defaults.setProperty(PROPERTY_IDENTITY, identity.get());
if (credential != null)
defaults.setProperty(PROPERTY_CREDENTIAL, credential);
if (overrides.isPresent())
putAllAsString(overrides.get(), defaults);
putAllAsString(propertiesPrefixedWithJcloudsApiOrProviderId(getSystemProperties(), apiMetadata.getId(), providerId), defaults);
return defaults;
}
private static void putAllAsString(Map<?, ?> source, Properties target) {
for (Map.Entry<?, ?> entry : source.entrySet()) {
target.setProperty(entry.getKey().toString(), entry.getValue().toString());
}
}
@VisibleForTesting
protected Properties getSystemProperties() {
return System.getProperties();
}
public static Injector buildInjector(String name, ProviderMetadata providerMetadata, Supplier<Credentials> creds, List<Module> inputModules) {
List<Module> modules = newArrayList();
modules.addAll(inputModules);
boolean apiModuleSpecifiedByUser = apiModulePresent(inputModules);
Iterable<Module> defaultModules = ifSpecifiedByUserDontIncludeDefaultApiModule(
providerMetadata.getApiMetadata(), apiModuleSpecifiedByUser);
addAll(modules, defaultModules);
addClientModuleIfNotPresent(providerMetadata.getApiMetadata(), modules);
addRestContextBinding(providerMetadata.getApiMetadata(), modules);
addLoggingModuleIfNotPresent(modules);
addHttpModuleIfNeededAndNotPresent(modules);
addExecutorServiceIfNotPresent(modules);
addEventBusIfNotPresent(modules);
addCredentialStoreIfNotPresent(modules);
modules.add(new LifeCycleModule());
modules.add(new BindProviderMetadataContextAndCredentials(providerMetadata, creds));
modules.add(new BindNameToContext(name));
Injector returnVal = Guice.createInjector(GUICE_STAGE, modules);
returnVal.getInstance(ExecutionList.class).execute();
return returnVal;
}
static Properties resolveProperties(Properties mutable, String providerId, Set<String> keys, Set<String> optionalKeys) throws NoSuchElementException {
for (String key : keys) {
String scopedProperty = Iterables.get(Splitter.on('.').split(key), 1);
try {
mutable.setProperty(key, searchPropertiesForProviderScopedProperty(mutable, providerId, scopedProperty));
} catch (NoSuchElementException e) {
if (!optionalKeys.contains(key))
throw e;
}
}
return mutable;
}
static void addRestContextBinding(ApiMetadata apiMetadata, List<Module> modules) {
if (apiMetadata instanceof HttpApiMetadata) {
try {
modules
.add(new BindApiContextWithWildcardExtendsExplicitAndRawType(HttpApiMetadata.class.cast(apiMetadata)));
} catch (IllegalArgumentException ignored) {
}
}
}
static Iterable<Module> ifSpecifiedByUserDontIncludeDefaultApiModule(ApiMetadata apiMetadata,
boolean restModuleSpecifiedByUser) {
Iterable<Module> defaultModules = transform(apiMetadata.getDefaultModules(),
new Function<Class<? extends Module>, Module>() {
@Override
public Module apply(Class<? extends Module> arg0) {
try {
return arg0.getConstructor().newInstance();
} catch (InstantiationException e) {
throw propagate(e);
} catch (IllegalAccessException e) {
throw propagate(e);
} catch (InvocationTargetException e) {
throw propagate(e);
} catch (NoSuchMethodException e) {
throw propagate(e);
}
}
});
if (restModuleSpecifiedByUser)
defaultModules = filter(defaultModules, not(configuresApi));
return defaultModules;
}
@SuppressWarnings( { "unchecked" })
static Map<String, Object> propertiesPrefixedWithJcloudsApiOrProviderId(Properties properties, String apiId,
String providerId) {
return filterKeys(Map.class.cast(properties), containsPattern("^(jclouds|" + providerId + "|" + apiId + ").*"));
}
@VisibleForTesting
static void addLoggingModuleIfNotPresent(List<Module> modules) {
if (!any(modules, instanceOf(LoggingModule.class)))
modules.add(new JDKLoggingModule());
}
@VisibleForTesting
static void addHttpModuleIfNeededAndNotPresent(List<Module> modules) {
if (nothingConfiguresAnHttpService(modules))
modules.add(new JavaUrlHttpCommandExecutorServiceModule());
}
static boolean nothingConfiguresAnHttpService(List<Module> modules) {
return !any(modules, new Predicate<Module>() {
public boolean apply(Module input) {
return input.getClass().isAnnotationPresent(ConfiguresHttpCommandExecutorService.class);
}
});
}
@VisibleForTesting
static void addClientModuleIfNotPresent(ApiMetadata apiMetadata, List<Module> modules) {
if (!apiModulePresent(modules)) {
addClientModule(apiMetadata, modules);
}
}
private static boolean apiModulePresent(List<Module> modules) {
return any(modules, configuresApi);
}
private static Predicate<Module> configuresApi = new Predicate<Module>() {
public boolean apply(Module input) {
return input.getClass().isAnnotationPresent(ConfiguresHttpApi.class);
}
};
@SuppressWarnings({ "unchecked", "rawtypes" })
static void addClientModule(ApiMetadata apiMetadata, List<Module> modules) {
// TODO: move this up
if (apiMetadata instanceof HttpApiMetadata) {
HttpApiMetadata api = HttpApiMetadata.class.cast(apiMetadata);
modules.add(new HttpApiModule(api.getApi()));
} else {
modules.add(new RestModule());
// Minimally bind HttpClient so that Utils works.
modules.add(new AbstractModule() {
@Override public void configure() {
bind(new TypeLiteral<Function<Invocation, Object>>() {
}).to(InvokeHttpMethod.class);
bindHttpApi(binder(), HttpClient.class);
}
});
}
}
@VisibleForTesting
static void addEventBusIfNotPresent(List<Module> modules) {
if (!any(modules, new Predicate<Module>() {
public boolean apply(Module input) {
return input.getClass().isAnnotationPresent(ConfiguresEventBus.class);
}
}
)) {
modules.add(new EventBusModule());
}
}
@VisibleForTesting
static void addExecutorServiceIfNotPresent(List<Module> modules) {
if (!any(modules, new Predicate<Module>() {
public boolean apply(Module input) {
return input.getClass().isAnnotationPresent(ConfiguresExecutorService.class);
}
}
)) {
if (any(modules, new Predicate<Module>() {
public boolean apply(Module input) {
return input.getClass().isAnnotationPresent(SingleThreaded.class);
}
})) {
modules.add(new ExecutorServiceModule(newDirectExecutorService()));
} else {
modules.add(new ExecutorServiceModule());
}
}
}
@VisibleForTesting
static void addCredentialStoreIfNotPresent(List<Module> modules) {
if (!any(modules, new Predicate<Module>() {
public boolean apply(Module input) {
return input.getClass().isAnnotationPresent(ConfiguresCredentialStore.class);
}
}
)) {
modules.add(new CredentialStoreModule());
}
}
/**
* Builds the base context for this api. Note that this may be of type {@link Closer}, if nothing
* else was configured via {@link ApiMetadata#getContext()}. Typically, the type returned is
* {@link ApiContext}
*
* @see ApiMetadata#getContext()
* @see #build(TypeToken)
*/
@SuppressWarnings("unchecked")
public <C extends Context> C build() {
return (C) build(apiMetadata.getContext());
}
/**
* @see #buildView(Class)
*/
public <V extends View> V build(Class<V> viewType) {
return buildView(checkNotNull(viewType, "viewType"));
}
/**
* @see #buildView(TypeToken)
*/
public <V extends View> V buildView(Class<V> viewType) {
return buildView(typeToken(viewType));
}
/**
* this will build any {@link ApiMetadata#getViews() view} supported by the ApiMetadata.
*
* ex. {@code builder.build(BlobStoreContext.class) } will work, if {@code TypeToken<BlobStore>}
* is a configured {@link ApiMetadata#getViews() view} of this api.
*
*/
@SuppressWarnings("unchecked")
public <V extends View> V buildView(TypeToken<V> viewType) {
TypeToken<V> returnType;
try {
returnType = (TypeToken<V>) Apis.findView(apiMetadata, checkNotNull(viewType, "viewType"));
} catch (NoSuchElementException e) {
throw new IllegalArgumentException(String.format(
"api %s not wrappable as %s; context: %s, views: %s", apiMetadata,
viewType, apiMetadata.getContext(), apiMetadata.getViews()));
}
return (V) buildInjector().getInstance(Key.get(TypeLiteral.get(returnType.getType())));
}
/**
* this will build the {@link ApiMetadata#getContext() context} supported by the current ApiMetadata.
*/
@SuppressWarnings("unchecked")
public <C extends Context> C build(TypeToken<C> contextType) {
TypeToken<C> returnType = null;
if (TypeTokenUtils.isSupertypeOf(contextType, apiMetadata.getContext()))
returnType = (TypeToken<C>) apiMetadata.getContext();
else
throw new IllegalArgumentException(String.format("api %s not assignable from %s; context: %s", apiMetadata,
contextType, apiMetadata.getContext()));
return (C) buildInjector().getInstance(Key.get(TypeLiteral.get(returnType.getType())));
}
/**
* This will return the top-level interface for the api or provider.
*
* Ex.
* <pre>
* api = ContextBuilder.newBuilder("openstack-nova")
* ...
* .buildApi(NovaApi.class);
*</pre>
*/
public <A extends Closeable> A buildApi(Class<A> api) {
return buildApi(typeToken(api));
}
/**
* like {@link #buildApi(Class)} but permits a type-token for convenience.
*/
@SuppressWarnings("unchecked")
public <A extends Closeable> A buildApi(TypeToken<A> apiType) {
return (A) buildInjector().getInstance(Key.get(TypeLiteral.get(apiType.getType())));
}
public ApiMetadata getApiMetadata() {
return apiMetadata;
}
}