/*
* The MIT License
*
* Copyright 2015 Tim Boudreau.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.mastfrog.giulius.mongodb.async;
import com.google.inject.AbstractModule;
import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.mastfrog.asyncpromises.mongo.CollectionPromises;
import com.mastfrog.giulius.Dependencies;
import com.mastfrog.util.Checks;
import com.mastfrog.util.ConfigurationError;
import com.mongodb.async.client.MongoClient;
import com.mongodb.async.client.MongoClientSettings;
import com.mongodb.async.client.MongoCollection;
import com.mongodb.async.client.MongoDatabase;
import com.mongodb.client.model.CreateCollectionOptions;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import static java.util.Arrays.asList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.bson.Document;
import org.bson.codecs.BsonValueCodecProvider;
import org.bson.codecs.Codec;
import org.bson.codecs.DocumentCodecProvider;
import org.bson.codecs.ValueCodecProvider;
import org.bson.codecs.configuration.CodecConfigurationException;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
/**
* Supports MongoDB Guice bindings, allowing @Named to be used to inject
* collections, and similar. Binds the following
* <ul>
* <li>MongoCLientSettings (optional - you can provide your own) - the default
* one is initialized from settings properties, using key names defined as
* constant string fields on this class</li>
* <li>CodecRegistry (used by MongoClientSettings if you don't provide your
* own)</li>
* <li>MongoClient</li>
* <li>MongoDatabase</li>
* <li>MongoCollection - bind multiple collections and inject them using
* @Named - if you specify a type, you can inject MongoCollection
* parameterized on either your type or Document</li>
* <li>CollectionPromises - bind multiple collections and inject a
* promise/builder based way to access them using @Named - if you specify a
* type, you can inject MongoCollection parameterized on either your type or
* Document</li>
* </ul>
*
* @author Tim Boudreau
*/
public class GiuliusMongoAsyncModule extends AbstractModule implements MongoAsyncConfig<GiuliusMongoAsyncModule> {
private MongoClientSettings settings;
private volatile boolean done;
private final Set<CollectionBinding<?>> bindings = new HashSet<>();
private final Set<Class<? extends MongoAsyncInitializer>> initializers = new HashSet<>();
private final Set<CodecProvider> codecProviders = new HashSet<>();
private final Set<Codec<?>> codecs = new HashSet<>();
private final Set<Class<? extends Codec<?>>> codecTypes = new HashSet<>();
private final Set<Class<? extends CodecProvider>> codecProviderTypes = new HashSet<>();
/**
* Add a codec to decode objects from BSON.
*
* @param prov The provider
* @return this
*/
@Override
public GiuliusMongoAsyncModule withCodecProvider(CodecProvider prov) {
checkDone();
checkSettings();
codecProviders.add(prov);
return this;
}
private void checkSettings() {
if (settings != null) {
throw new ConfigurationError("You are providing your own "
+ "MongoClientSettings - set up its codec registry directly"
+ "in that case");
}
}
/**
* Add a codec to decode objects from BSON.
*
* @param prov The codec
* @return this
*/
@Override
public GiuliusMongoAsyncModule withCodec(Codec<?> prov) {
checkDone();
checkSettings();
codecs.add(prov);
return this;
}
/**
* Add a codec to decode objects from BSON, by type. The passed
* CodecProvider will be instantiated by Guice and may contain injected
* elements.
*
* @param prov The provider
* @return this
*/
@Override
public GiuliusMongoAsyncModule withCodecProvider(Class<? extends CodecProvider> prov) {
checkDone();
checkSettings();
codecProviderTypes.add(prov);
return this;
}
/**
* Add a codec to decode objects from BSON, by type. The passed Codec will
* be instantiated by Guice and may contain injected elements.
*
* @param prov The provider
* @return this
*/
@Override
public GiuliusMongoAsyncModule withCodec(Class<? extends Codec<?>> prov) {
checkDone();
checkSettings();
codecTypes.add(prov);
return this;
}
private Class<? extends DynamicCodecs> dynCodecs;
public GiuliusMongoAsyncModule withDynamicCodecs(Class<? extends DynamicCodecs> codecs) {
checkDone();
if (dynCodecs != null) {
throw new ConfigurationError("Dynamic codecs already set");
}
this.dynCodecs = codecs;
return this;
}
static class DefaultFallbackCodecs implements DynamicCodecs {
@Override
public <T> Codec<T> createCodec(Class<T> type, CodecConfigurationException ex) {
throw ex;
}
}
@Singleton
private class CodecRegistryImpl implements CodecRegistry {
private final Provider<Dependencies> deps;
private CodecRegistry registry;
private final Provider<DynamicCodecs> fallback;
CodecRegistryImpl(Provider<Dependencies> deps, Provider<DynamicCodecs> fallback) {
this.deps = deps;
this.fallback = fallback;
}
private CodecRegistry get() {
if (registry != null) {
return registry;
}
Dependencies deps = this.deps.get();
List<CodecProvider> providers = new LinkedList<>(GiuliusMongoAsyncModule.this.codecProviders);
List<Codec<?>> codecs = new LinkedList<>(GiuliusMongoAsyncModule.this.codecs);
for (Class<? extends CodecProvider> c : codecProviderTypes) {
providers.add(deps.getInstance(c));
}
for (Class<? extends Codec<?>> c : codecTypes) {
codecs.add(deps.getInstance(c));
}
int total = providers.size() + codecs.size();
if (total == 0) {
return DEFAULT_CODEC_REGISTRY;
}
List<CodecRegistry> all = new LinkedList<>();
if (!codecs.isEmpty()) {
CodecRegistry forProviders = CodecRegistries.fromCodecs(codecs);
all.add(forProviders);
}
if (!providers.isEmpty()) {
CodecRegistry forCodecs = CodecRegistries.fromProviders(providers);
all.add(forCodecs);
}
all.add(DEFAULT_CODEC_REGISTRY);
return registry = CodecRegistries.fromRegistries(all);
}
@Override
public <T> Codec<T> get(Class<T> type) {
try {
return get().get(type);
} catch (CodecConfigurationException ex) {
return fallback.get().createCodec(type, ex);
}
}
}
// XXX when 3.1.0 is stable, replace with MongoClients.getDefaultCodecRegistry()
private static final CodecRegistry DEFAULT_CODEC_REGISTRY
= fromProviders(asList(new ValueCodecProvider(),
new DocumentCodecProvider(),
new BsonValueCodecProvider()));
private void checkDone() {
if (done) {
throw new ConfigurationError("Cannot configure module after the injector has been initialized");
}
}
/**
* Use the passed client settings instead of deriving them from settings
* key/value pairs.
*
* @param settings The settings
* @return this
*/
@Override
public GiuliusMongoAsyncModule withClientSettings(MongoClientSettings settings) {
checkDone();
this.settings = settings;
return this;
}
/**
* Bind the collection with the passed name to @Named DBCollection of
* the same name, so it is injectable as
* <code>@Named("someBinding") MongoCollection<MyType></code>.
* where <code>MyType</code> is tha passed type.
*
* @param bindingName The binding and collection name
* @return this
*/
public <T> GiuliusMongoAsyncModule bindCollection(String bindingName, Class<T> type) {
return bindCollection(bindingName, bindingName, type);
}
/**
* Bind a collection so it is injectable as
* <code>@Named("someBinding") MongoCollection<Document></code>.
*
* @param bindingName The name of the binding <i>and</i> the collection in
* question
* @return this
*/
public GiuliusMongoAsyncModule bindCollection(String bindingName) {
bindCollection(bindingName, bindingName);
return this;
}
/**
* Add an initializer which will be called when collections are created, and
* before and after the <code>MongoClient</code> is initialized. This can
* also be accomplished by simply binding the initializer as an eager
* singleton, if the module in question does not have direct access to this
* one.
*
* @param initializerType The type of initializer
* @return this
*/
public GiuliusMongoAsyncModule withInitializer(Class<? extends MongoAsyncInitializer> initializerType) {
checkDone();
initializers.add(initializerType);
return this;
}
/**
* Bind the collection with the passed name to @Named DBCollection wth
* the passed binding name
*
* @param bindingName The binding used in @Named
* @param collectionName The collection name
* @return this
*/
@Override
public GiuliusMongoAsyncModule bindCollection(String bindingName, String collectionName) {
checkDone();
bindings.add(new CollectionBinding<Document>(collectionName, bindingName, null, Document.class));
return this;
}
/**
* Bind the collection with the passed name to @Named DBCollection of
* the same name, so it is injectable as
* <code>@Named("someBinding") MongoCollection<MyType></code>.
* where <code>MyType</code> is tha passed type.
*
* @param bindingName The name of the binding that will appear in
* @Named annotations
* @param collectionName The name of the collection to be created/used in
* MongoDB, which may differ from the binding name
* @return this
*/
@Override
public <T> GiuliusMongoAsyncModule bindCollection(String bindingName, String collectionName, Class<T> type) {
checkDone();
bindings.add(new CollectionBinding<T>(collectionName, bindingName, null, type));
return this;
}
@Override
protected void configure() {
for (Class<? extends MongoAsyncInitializer> itype : this.initializers) {
bind(itype).asEagerSingleton();;
}
if (settings != null) {
bind(MongoClientSettings.class).toInstance(settings);
} else {
bind(MongoClientSettings.class).toProvider(MongoClientSettingsProvider.class).asEagerSingleton();
}
if (this.dynCodecs != null) {
bind(DynamicCodecs.class).to(this.dynCodecs);
}
bind(CodecRegistry.class).toInstance(new CodecRegistryImpl(binder().getProvider(Dependencies.class), binder().getProvider(DynamicCodecs.class)));
// Bind this as an eager singleton so that the client shutdown hook runs before the
// MongoHarness shutdown hook shuts down the server - otherwise, can get exceptions thrown
// during shutdown
bind(MongoClient.class).toProvider(AsyncMongoClientProvider.class);
bind(MongoDatabase.class).toProvider(MongoDatabaseProvider.class).in(Scopes.SINGLETON);
for (CollectionBinding<?> binding : bindings) {
binding.bind(binder());
}
}
private static final class CollectionBinding<T> {
private final String collection;
private final String bindingName;
private final CreateCollectionOptions opts;
private Class<T> type;
public CollectionBinding(String collection, String bindingName, CreateCollectionOptions opts, Class<T> type) {
Checks.notNull("collection", collection);
this.collection = collection;
this.bindingName = bindingName == null ? collection : bindingName;
this.opts = opts != null ? opts : new CreateCollectionOptions();
this.type = type;
}
@Override
public boolean equals(Object obj) {
return obj instanceof CollectionBinding && ((CollectionBinding) obj).collection.equals(collection);
}
@Override
public int hashCode() {
return collection.hashCode() * 37;
}
@Override
public String toString() {
return collection + ":" + bindingName + " with " + opts;
}
@SuppressWarnings("unchecked")
void bind(Binder binder) {
Provider<MongoDatabase> dbProvider = binder.getProvider(MongoDatabase.class);
Provider<KnownCollections> knownProvider = binder.getProvider(KnownCollections.class);
Provider<MongoAsyncInitializer.Registry> inits = binder.getProvider(MongoAsyncInitializer.Registry.class);
MongoTypedCollectionProvider<Document> docProvider = new MongoTypedCollectionProvider<>(dbProvider, collection, Document.class, knownProvider, opts, inits);
CollectionPromisesProvider<Document> cpProvider = new CollectionPromisesProvider<>(docProvider);
binder.bind(COLLECTION_PROMISES).annotatedWith(Names.named(bindingName)).toProvider(cpProvider);
binder.bind(MONGO_DOCUMENT_COLLECTION).annotatedWith(Names.named(bindingName)).toProvider(docProvider);
if (type != Document.class) {
MongoTypedCollectionProvider<T> typedProvider = new MongoTypedCollectionProvider<T>(dbProvider, collection, type, knownProvider, opts, inits);
Type t = new FakeType<>(type);
Key<MongoCollection<T>> key = (Key<MongoCollection<T>>) Key.get(t, Names.named(bindingName));
binder.bind(key).toProvider(typedProvider);
CollectionPromisesProvider<T> promises = new CollectionPromisesProvider<>(typedProvider);
Type ct = new FakeType2<>(type);
Key<CollectionPromises<T>> promiseKey = (Key<CollectionPromises<T>>) Key.get(ct, Names.named(bindingName));
binder.bind(promiseKey).toProvider(promises);
}
}
}
/**
* TypeLiteral for MongoCollection parameterized on BSON Document.
*/
public static final TypeLiteral<MongoCollection<Document>> MONGO_DOCUMENT_COLLECTION = new TL();
public static final TypeLiteral<CollectionPromises<Document>> COLLECTION_PROMISES = new CPL();
static class TL extends TypeLiteral<MongoCollection<Document>> {
}
static class CPL extends TypeLiteral<CollectionPromises<Document>> {
}
static class FakeType<T> implements ParameterizedType {
private final Class<T> genericType;
public FakeType(Class<T> genericType) {
this.genericType = genericType;
}
public String getTypeName() {
return MongoCollection.class.getName();
}
public Type[] getActualTypeArguments() {
return new Type[]{genericType};
}
public Type getRawType() {
return MongoCollection.class;
}
public Type getOwnerType() {
return null;
}
}
static class FakeType2<T> implements ParameterizedType {
private final Class<T> genericType;
public FakeType2(Class<T> genericType) {
this.genericType = genericType;
}
public String getTypeName() {
return CollectionPromises.class.getName();
}
public Type[] getActualTypeArguments() {
return new Type[]{genericType};
}
public Type getRawType() {
return CollectionPromises.class;
}
public Type getOwnerType() {
return null;
}
}
static class CollectionPromisesProvider<T> implements Provider<CollectionPromises<T>> {
private final MongoTypedCollectionProvider<T> prov;
public CollectionPromisesProvider(MongoTypedCollectionProvider<T> prov) {
this.prov = prov;
}
@Override
public CollectionPromises<T> get() {
return new CollectionPromises<>(prov.get());
}
}
}