From 3961c41bbc544353325b9cd84a507dbccc09223e Mon Sep 17 00:00:00 2001 From: Adrien LAUER Date: Mon, 10 Aug 2015 19:08:43 +0200 Subject: [PATCH] Initial MongoDB support (sync and async) --- persistence-support/mongodb/pom.xml | 98 ++++++++++ .../persistence/mongodb/MongoDBAsyncIT.java | 80 ++++++++ .../seed/persistence/mongodb/MongoDBIT.java | 59 ++++++ ...org.seedstack.seed.persistence.mongo.props | 29 +++ .../mongodb/src/it/resources/logback-test.xml | 22 +++ .../mongodb/api/MongoDbErrorCodes.java | 24 +++ .../internal/AbstractMongoDbManager.java | 155 +++++++++++++++ .../mongodb/internal/AsyncMongoDbManager.java | 177 ++++++++++++++++++ .../mongodb/internal/MongoDbManager.java | 23 +++ .../mongodb/internal/MongoDbModule.java | 59 ++++++ .../mongodb/internal/MongoDbPlugin.java | 148 +++++++++++++++ .../mongodb/internal/SyncMongoDbManager.java | 174 +++++++++++++++++ ...e.mongodb.api.MongoDbErrorCodes.properties | 15 ++ .../services/io.nuun.kernel.api.Plugin | 1 + .../src/test/resources/logback-test.xml | 22 +++ persistence-support/pom.xml | 1 + 16 files changed, 1087 insertions(+) create mode 100644 persistence-support/mongodb/pom.xml create mode 100644 persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBAsyncIT.java create mode 100644 persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBIT.java create mode 100644 persistence-support/mongodb/src/it/resources/META-INF/configuration/org.seedstack.seed.persistence.mongo.props create mode 100644 persistence-support/mongodb/src/it/resources/logback-test.xml create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/api/MongoDbErrorCodes.java create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AbstractMongoDbManager.java create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AsyncMongoDbManager.java create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbManager.java create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbModule.java create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbPlugin.java create mode 100644 persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/SyncMongoDbManager.java create mode 100644 persistence-support/mongodb/src/main/resources/META-INF/errors/org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes.properties create mode 100644 persistence-support/mongodb/src/main/resources/META-INF/services/io.nuun.kernel.api.Plugin create mode 100644 persistence-support/mongodb/src/test/resources/logback-test.xml diff --git a/persistence-support/mongodb/pom.xml b/persistence-support/mongodb/pom.xml new file mode 100644 index 000000000..5c18949f9 --- /dev/null +++ b/persistence-support/mongodb/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + + org.seedstack.seed + seed-persistence-support + 2.0.1-SNAPSHOT + + + seed-persistence-support-mongodb + + + true + 3.0.3 + + + + + org.seedstack.seed + seed-core-support-core + ${project.version} + + + org.mongodb + mongo-java-driver + ${mongodb.version} + provided + + + org.mongodb + mongodb-driver-async + ${mongodb.version} + provided + + + + org.seedstack.seed + seed-unittest-support + ${project.version} + test + + + org.seedstack.seed + seed-integrationtest-support-core + ${project.version} + test + + + ch.qos.logback + logback-classic + test + + + + + + + com.syncleus.maven.plugins + maven-mongodb-plugin + 1.1.1 + + + + + + start-mongodb + + start + + + + stop-mongodb + + stop + + + + + + + diff --git a/persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBAsyncIT.java b/persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBAsyncIT.java new file mode 100644 index 000000000..5e90f8c15 --- /dev/null +++ b/persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBAsyncIT.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb; + +import com.mongodb.async.SingleResultCallback; +import com.mongodb.async.client.MongoClient; +import com.mongodb.async.client.MongoDatabase; +import org.bson.Document; +import org.junit.Test; +import org.seedstack.seed.it.AbstractSeedIT; + +import javax.inject.Inject; +import javax.inject.Named; + +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MongoDbAsyncIT extends AbstractSeedIT { + @Inject + @Named("client2") + MongoClient client2; + + @Inject + @Named("db2") + MongoDatabase db2; + + @Inject + @Named("client3") + MongoClient client3; + + @Inject + @Named("db3") + MongoDatabase db3; + + @Test + public void mongo_clients_are_injectable() { + assertThat(client2).isNotNull(); + assertThat(client3).isNotNull(); + } + + @Test + public void mongo_databases_are_injectable() { + assertThat(db2).isNotNull(); + assertThat(db3).isNotNull(); + } + + @Test + public void test_insert_into_collection() throws InterruptedException { + Document doc = new Document("name", "MongoDB") + .append("type", "database") + .append("count", 1) + .append("info", new Document("x", 203).append("y", 102)); + + final Object mon = new Object(); + final AtomicBoolean inserted = new AtomicBoolean(false); + db2.getCollection("test1").insertOne(doc, new SingleResultCallback() { + @Override + public void onResult(Void result, Throwable t) { + assertThat(t).isNull(); + synchronized (mon) { + inserted.set(true); + mon.notify(); + } + } + }); + + synchronized (mon) { + mon.wait(5000); + assertThat(inserted.get()).isTrue(); + } + } +} diff --git a/persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBIT.java b/persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBIT.java new file mode 100644 index 000000000..a6887fc38 --- /dev/null +++ b/persistence-support/mongodb/src/it/java/org/seedstack/seed/persistence/mongodb/MongoDBIT.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb; + +import com.mongodb.MongoClient; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.Test; +import org.seedstack.seed.it.AbstractSeedIT; + +import javax.inject.Inject; +import javax.inject.Named; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MongoDbIT extends AbstractSeedIT { + @Inject + @Named("client1") + MongoClient client1; + + @Inject + @Named("db1") + MongoDatabase db1; + + @Inject + MongoClient implicitClient; + + @Inject + MongoDatabase implicitDatabase; + + @Test + public void mongo_clients_are_injectable() { + assertThat(client1).isNotNull(); + assertThat(implicitClient).isNotNull(); + } + + @Test + public void mongo_databases_are_injectable() { + assertThat(db1).isNotNull(); + assertThat(implicitDatabase).isNotNull(); + } + + @Test + public void test_insert_into_collection() { + Document doc = new Document("name", "MongoDB") + .append("type", "database") + .append("count", 1) + .append("info", new Document("x", 203).append("y", 102)); + + db1.getCollection("test1").insertOne(doc); + } +} diff --git a/persistence-support/mongodb/src/it/resources/META-INF/configuration/org.seedstack.seed.persistence.mongo.props b/persistence-support/mongodb/src/it/resources/META-INF/configuration/org.seedstack.seed.persistence.mongo.props new file mode 100644 index 000000000..cddc2a04b --- /dev/null +++ b/persistence-support/mongodb/src/it/resources/META-INF/configuration/org.seedstack.seed.persistence.mongo.props @@ -0,0 +1,29 @@ +# +# Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. +# +# This file is part of SeedStack, An enterprise-oriented full development stack. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +[org.seedstack.seed.persistence.mongodb] +clients = client1, client2, client3 + +[org.seedstack.seed.persistence.mongodb.client.client1] +hosts = localhost +option.connectionsPerHost = 50 +databases = db1 + +[org.seedstack.seed.persistence.mongodb.client.client2] +async = true +hosts = localhost +setting.connectionPool.maxSize = 50 +databases = db2 + +[org.seedstack.seed.persistence.mongodb.client.client3] +async = true +hosts = localhost +databases = db2 +alias.db2 = db3 diff --git a/persistence-support/mongodb/src/it/resources/logback-test.xml b/persistence-support/mongodb/src/it/resources/logback-test.xml new file mode 100644 index 000000000..f7e675181 --- /dev/null +++ b/persistence-support/mongodb/src/it/resources/logback-test.xml @@ -0,0 +1,22 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + \ No newline at end of file diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/api/MongoDbErrorCodes.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/api/MongoDbErrorCodes.java new file mode 100644 index 000000000..028358c12 --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/api/MongoDbErrorCodes.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.api; + +import org.seedstack.seed.core.api.ErrorCode; + +public enum MongoDbErrorCodes implements ErrorCode { + MISSING_URI, + UNABLE_TO_PARSE_SERVER_ADDRESS, + UNSUPPORTED_AUTHENTICATION_MECHANISM, + UNKNOWN_CLIENT_SPECIFIED, + DUPLICATE_DATABASE_NAME, + UNABLE_TO_INSTANTIATE_CLASS, + UNKNOWN_CLIENT_OPTION, + UNKNOWN_CLIENT_SETTING, + MISSING_HOSTS_CONFIGURATION +} diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AbstractMongoDbManager.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AbstractMongoDbManager.java new file mode 100644 index 000000000..62126757b --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AbstractMongoDbManager.java @@ -0,0 +1,155 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.internal; + +import com.google.inject.Module; +import com.mongodb.AuthenticationMechanism; +import com.mongodb.MongoCredential; +import com.mongodb.ServerAddress; +import org.apache.commons.configuration.Configuration; +import org.seedstack.seed.core.api.SeedException; +import org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +abstract class AbstractMongoDbManager implements MongoDbManager { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMongoDbManager.class); + + private final Map mongoClients = new HashMap(); + private final Map mongoDatabases = new HashMap(); + + @Override + public void registerClient(String clientName, Configuration clientConfiguration) { + LOGGER.info("Creating MongoDB client {}", clientName); + mongoClients.put(clientName, doCreateClient(clientConfiguration)); + } + + @Override + public void registerDatabase(String clientName, String dbName, String alias) { + C mongoClient = mongoClients.get(clientName); + + if (mongoClient == null) { + throw SeedException.createNew(MongoDbErrorCodes.UNKNOWN_CLIENT_SPECIFIED).put("clientName", clientName).put("dbName", dbName); + } + + mongoDatabases.put(alias, doCreateDatabase(mongoClient, dbName)); + } + + @Override + public void shutdown() { + try { + for (Map.Entry mongoClientEntry : mongoClients.entrySet()) { + LOGGER.info("Closing MongoDB client {}", mongoClientEntry.getKey()); + try { + doClose(mongoClientEntry.getValue()); + } catch (Exception e) { + LOGGER.warn(String.format("Unable to properly close MongoDB client %s", mongoClientEntry.getKey()), e); + } + } + } finally { + mongoDatabases.clear(); + mongoClients.clear(); + } + } + + @Override + @SuppressWarnings("unchecked") + public Module getModule() { + Type[] actualTypeArguments = ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments(); + return new MongoDbModule((Class) actualTypeArguments[0], (Class) actualTypeArguments[1], mongoClients, mongoDatabases); + } + + protected T instanceFromClassName(Class clazz, String className) { + try { + return Class.forName(className).asSubclass(clazz).newInstance(); + } catch (Exception e) { + throw SeedException.wrap(e, MongoDbErrorCodes.UNABLE_TO_INSTANTIATE_CLASS).put("className", className); + } + } + + public List buildServerAddresses(String[] addresses) { + List serverAddresses = new ArrayList(); + + if (addresses != null) { + for (String address : addresses) { + String[] split = address.split(":", 2); + if (split.length == 1) { + serverAddresses.add(new ServerAddress(split[0])); + } else if (split.length == 2) { + serverAddresses.add(new ServerAddress(split[0], Integer.parseInt(split[1]))); + } else { + throw SeedException.createNew(MongoDbErrorCodes.UNABLE_TO_PARSE_SERVER_ADDRESS).put("address", address); + } + } + } + + return serverAddresses; + } + + protected List buildMongoCredentials(Configuration clientConfiguration) { + List mongoCredentials = new ArrayList(); + Configuration credentialConfiguration; + int i = 0; + + while (!(credentialConfiguration = clientConfiguration.subset(String.format("credential[%d]", i++))).isEmpty()) { + mongoCredentials.add(buildMongoCredential(credentialConfiguration)); + } + + return mongoCredentials; + } + + protected MongoCredential buildMongoCredential(Configuration credentialConfiguration) { + AuthenticationMechanism authenticationMechanism = null; + String mechanism = credentialConfiguration.getString("mechanism"); + if (mechanism != null) { + authenticationMechanism = AuthenticationMechanism.fromMechanismName(mechanism); + } + + String user = credentialConfiguration.getString("user"); + String source = credentialConfiguration.getString("source"); + char[] password = credentialConfiguration.getString("password").toCharArray(); + + if (authenticationMechanism != null) { + switch (authenticationMechanism) { + case PLAIN: + return MongoCredential.createPlainCredential(user, source, password); + case MONGODB_CR: + return MongoCredential.createMongoCRCredential(user, source, password); + case SCRAM_SHA_1: + return MongoCredential.createScramSha1Credential(user, source, password); + case MONGODB_X509: + return MongoCredential.createMongoX509Credential(user); + case GSSAPI: + return MongoCredential.createGSSAPICredential(user); + default: + throw SeedException.createNew(MongoDbErrorCodes.UNSUPPORTED_AUTHENTICATION_MECHANISM).put("mechanism", authenticationMechanism.getMechanismName()); + } + } else { + return MongoCredential.createCredential( + user, + source, + password + ); + } + } + + protected abstract C doCreateClient(Configuration clientConfiguration); + + protected abstract D doCreateDatabase(C client, String dbName); + + protected abstract void doClose(C client); +} diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AsyncMongoDbManager.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AsyncMongoDbManager.java new file mode 100644 index 000000000..311c251e6 --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/AsyncMongoDbManager.java @@ -0,0 +1,177 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.internal; + +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.async.client.MongoClient; +import com.mongodb.async.client.MongoClientSettings; +import com.mongodb.async.client.MongoClients; +import com.mongodb.async.client.MongoDatabase; +import com.mongodb.connection.ClusterConnectionMode; +import com.mongodb.connection.ClusterSettings; +import com.mongodb.connection.ClusterType; +import com.mongodb.connection.ConnectionPoolSettings; +import com.mongodb.connection.ServerSettings; +import com.mongodb.connection.SocketSettings; +import com.mongodb.connection.SslSettings; +import com.mongodb.selector.ServerSelector; +import org.apache.commons.configuration.Configuration; +import org.bson.codecs.configuration.CodecRegistry; +import org.seedstack.seed.core.api.SeedException; +import org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes; + +import java.util.Iterator; +import java.util.concurrent.TimeUnit; + +class AsyncMongoDbManager extends AbstractMongoDbManager { + @Override + protected MongoClient doCreateClient(Configuration clientConfiguration) { + String uri = clientConfiguration.getString("uri"); + if (uri != null && !uri.isEmpty()) { + return MongoClients.create(uri); + } else { + return MongoClients.create(buildMongoClientSettings(clientConfiguration)); + } + } + + @Override + protected MongoDatabase doCreateDatabase(MongoClient client, String dbName) { + return client.getDatabase(dbName); + } + + @Override + protected void doClose(MongoClient client) { + client.close(); + } + + private MongoClientSettings buildMongoClientSettings(Configuration clientConfiguration) { + MongoClientSettings.Builder settingsBuilder = MongoClientSettings.builder(); + ClusterSettings.Builder clusterSettingsBuilder = ClusterSettings.builder(); + SocketSettings.Builder socketSettingsBuilder = SocketSettings.builder(); + SocketSettings.Builder heartbeatSocketSettingsBuilder = SocketSettings.builder(); + ConnectionPoolSettings.Builder connectionSettingsBuilder = ConnectionPoolSettings.builder(); + ServerSettings.Builder serverSettingsBuilder = ServerSettings.builder(); + SslSettings.Builder sslSettingsBuilder = SslSettings.builder(); + + // Apply aliases + String[] hosts = clientConfiguration.getStringArray("hosts"); + if (hosts != null && hosts.length > 0) { + clusterSettingsBuilder.hosts(buildServerAddresses(hosts)); + } + + // Apply configuration properties + Iterator it = clientConfiguration.getKeys("setting"); + while (it.hasNext()) { + String key = it.next(); + String setting = key.substring(8); + + // cluster settings + if ("cluster.description".equals(setting)) { + clusterSettingsBuilder.description(clientConfiguration.getString(key)); + } else if ("cluster.hosts".equals(setting)) { + clusterSettingsBuilder.hosts(buildServerAddresses(clientConfiguration.getStringArray(key))); + } else if ("cluster.mode".equals(setting)) { + clusterSettingsBuilder.mode(ClusterConnectionMode.valueOf(clientConfiguration.getString(key))); + } else if ("cluster.requiredReplicaSetName".equals(setting)) { + clusterSettingsBuilder.requiredReplicaSetName(clientConfiguration.getString(key)); + } else if ("cluster.requiredClusterType".equals(setting)) { + clusterSettingsBuilder.requiredClusterType(ClusterType.valueOf(clientConfiguration.getString(key))); + } else if ("cluster.serverSelector".equals(setting)) { + clusterSettingsBuilder.serverSelector(instanceFromClassName(ServerSelector.class, clientConfiguration.getString(key))); + } else if ("cluster.serverSelectionTimeout".equals(setting)) { + clusterSettingsBuilder.serverSelectionTimeout(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } else if ("cluster.maxWaitQueueSize".equals(setting)) { + clusterSettingsBuilder.maxWaitQueueSize(clientConfiguration.getInt(key)); + } + + // socket settings + else if ("socket.connectTimeout".equals(setting)) { + socketSettingsBuilder.connectTimeout(clientConfiguration.getInt(key), TimeUnit.MILLISECONDS); + } else if ("socket.readTimeout".equals(setting)) { + socketSettingsBuilder.readTimeout(clientConfiguration.getInt(key), TimeUnit.MILLISECONDS); + } else if ("socket.keepAlive".equals(setting)) { + socketSettingsBuilder.keepAlive(clientConfiguration.getBoolean(key)); + } else if ("socket.receiveBufferSize".equals(setting)) { + socketSettingsBuilder.receiveBufferSize(clientConfiguration.getInt(key)); + } else if ("socket.sendBufferSize".equals(setting)) { + socketSettingsBuilder.sendBufferSize(clientConfiguration.getInt(key)); + } + + // heartbeat socket settings + else if ("heartbeatSocket.connectTimeout".equals(setting)) { + heartbeatSocketSettingsBuilder.connectTimeout(clientConfiguration.getInt(key), TimeUnit.MILLISECONDS); + } else if ("heartbeatSocket.readTimeout".equals(setting)) { + heartbeatSocketSettingsBuilder.readTimeout(clientConfiguration.getInt(key), TimeUnit.MILLISECONDS); + } else if ("heartbeatSocket.keepAlive".equals(setting)) { + heartbeatSocketSettingsBuilder.keepAlive(clientConfiguration.getBoolean(key)); + } else if ("heartbeatSocket.receiveBufferSize".equals(setting)) { + heartbeatSocketSettingsBuilder.receiveBufferSize(clientConfiguration.getInt(key)); + } else if ("heartbeatSocket.sendBufferSize".equals(setting)) { + heartbeatSocketSettingsBuilder.sendBufferSize(clientConfiguration.getInt(key)); + } + + // connection pool settings + else if ("connectionPool.maxSize".equals(setting)) { + connectionSettingsBuilder.maxSize(clientConfiguration.getInt(key)); + } else if ("connectionPool.minSize".equals(setting)) { + connectionSettingsBuilder.minSize(clientConfiguration.getInt(key)); + } else if ("connectionPool.maxWaitQueueSize".equals(setting)) { + connectionSettingsBuilder.maxWaitQueueSize(clientConfiguration.getInt(key)); + } else if ("connectionPool.maxWaitTime".equals(setting)) { + connectionSettingsBuilder.maxWaitTime(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } else if ("connectionPool.maxConnectionLifeTime".equals(setting)) { + connectionSettingsBuilder.maxConnectionLifeTime(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } else if ("connectionPool.maxConnectionIdleTime".equals(setting)) { + connectionSettingsBuilder.maxConnectionIdleTime(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } else if ("connectionPool.maintenanceInitialDelay".equals(setting)) { + connectionSettingsBuilder.maintenanceInitialDelay(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } else if ("connectionPool.maintenanceFrequency".equals(setting)) { + connectionSettingsBuilder.maintenanceFrequency(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } + + // server settings + else if ("server.heartbeatFrequency".equals(setting)) { + serverSettingsBuilder.heartbeatFrequency(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } else if ("server.minHeartbeatFrequency".equals(setting)) { + serverSettingsBuilder.minHeartbeatFrequency(clientConfiguration.getLong(key), TimeUnit.MILLISECONDS); + } + + // ssl settings + else if ("ssl.enabled".equals(setting)) { + sslSettingsBuilder.enabled(clientConfiguration.getBoolean(key)); + } else if ("ssl.invalidHostNameAllowed".equals(setting)) { + sslSettingsBuilder.invalidHostNameAllowed(clientConfiguration.getBoolean(key)); + } + + // global settings + else if ("readPreference".equals(setting)) { + settingsBuilder.readPreference(ReadPreference.valueOf(clientConfiguration.getString(key))); + } else if ("writeConcern".equals(setting)) { + settingsBuilder.writeConcern(WriteConcern.valueOf(clientConfiguration.getString(key))); + } else if ("codecRegistry".equals(setting)) { + settingsBuilder.codecRegistry(instanceFromClassName(CodecRegistry.class, clientConfiguration.getString(key))); + } else if (setting.startsWith("credential")) { + settingsBuilder.credentialList(buildMongoCredentials(clientConfiguration.subset("setting"))); + } else { + throw SeedException.createNew(MongoDbErrorCodes.UNKNOWN_CLIENT_SETTING).put("setting", setting); + } + } + + settingsBuilder.clusterSettings(clusterSettingsBuilder.build()); + settingsBuilder.socketSettings(socketSettingsBuilder.build()); + settingsBuilder.heartbeatSocketSettings(heartbeatSocketSettingsBuilder.build()); + settingsBuilder.connectionPoolSettings(connectionSettingsBuilder.build()); + settingsBuilder.serverSettings(serverSettingsBuilder.build()); + settingsBuilder.sslSettings(sslSettingsBuilder.build()); + + return settingsBuilder.build(); + } +} diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbManager.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbManager.java new file mode 100644 index 000000000..ba8bbe9d0 --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbManager.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.internal; + +import com.google.inject.Module; +import org.apache.commons.configuration.Configuration; + +interface MongoDbManager { + void registerClient(String clientName, Configuration clientConfiguration); + + void registerDatabase(String clientName, String dbName, String alias); + + Module getModule(); + + void shutdown(); +} diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbModule.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbModule.java new file mode 100644 index 000000000..9574cfd4d --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbModule.java @@ -0,0 +1,59 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.internal; + +import com.google.inject.AbstractModule; +import com.google.inject.Key; +import com.google.inject.name.Names; + +import java.util.Map; + +class MongoDbModule extends AbstractModule { + private final Map mongoClients; + private final Map mongoDatabases; + private final Class clientClass; + private final Class dbClass; + + MongoDbModule(Class clientClass, Class dbClass, Map mongoClients, Map mongoDatabases) { + this.clientClass = clientClass; + this.dbClass = dbClass; + this.mongoClients = mongoClients; + this.mongoDatabases = mongoDatabases; + } + + @Override + protected void configure() { + // bind clients + for (Map.Entry mongoClientEntry : mongoClients.entrySet()) { + bind(clientClass) + .annotatedWith(Names.named(mongoClientEntry.getKey())) + .toInstance(mongoClientEntry.getValue()); + } + + // if only one client is defined, define a linked binding without qualifier to it + if (mongoClients.size() == 1) { + bind(clientClass) + .to(Key.get(clientClass, Names.named(mongoClients.keySet().iterator().next()))); + } + + // bind databases + for (Map.Entry mongoDatabaseEntry : mongoDatabases.entrySet()) { + bind(dbClass) + .annotatedWith(Names.named(mongoDatabaseEntry.getKey())) + .toInstance(mongoDatabaseEntry.getValue()); + } + + // if only one database is defined, define a linked binding without qualifier to it + if (mongoDatabases.size() == 1) { + bind(dbClass) + .to(Key.get(dbClass, Names.named(mongoDatabases.keySet().iterator().next()))); + } + } +} diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbPlugin.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbPlugin.java new file mode 100644 index 000000000..004cd09ee --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/MongoDbPlugin.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.internal; + +import com.google.inject.AbstractModule; +import io.nuun.kernel.api.Plugin; +import io.nuun.kernel.api.plugin.InitState; +import io.nuun.kernel.api.plugin.PluginException; +import io.nuun.kernel.api.plugin.context.InitContext; +import io.nuun.kernel.core.AbstractPlugin; +import org.apache.commons.configuration.Configuration; +import org.seedstack.seed.core.api.Application; +import org.seedstack.seed.core.api.SeedException; +import org.seedstack.seed.core.internal.application.ApplicationPlugin; +import org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class MongoDbPlugin extends AbstractPlugin { + public static final String MONGO_PLUGIN_CONFIGURATION_PREFIX = "org.seedstack.seed.persistence.mongodb"; + + private static final Logger LOGGER = LoggerFactory.getLogger(MongoDbPlugin.class); + + private static class SyncHolder { + private static final MongoDbManager INSTANCE = new SyncMongoDbManager(); + } + + private static class AsyncHolder { + private static final MongoDbManager INSTANCE = new AsyncMongoDbManager(); + } + + private boolean hasSyncClients = false; + private boolean hasAsyncClients = false; + + @Override + public String name() { + return "seed-persistence-mongodb-plugin"; + } + + @Override + @SuppressWarnings("unchecked") + public InitState init(InitContext initContext) { + Application application = null; + Configuration mongoConfiguration = null; + + for (Plugin plugin : initContext.pluginsRequired()) { + if (plugin instanceof ApplicationPlugin) { + application = ((ApplicationPlugin) plugin).getApplication(); + mongoConfiguration = application.getConfiguration().subset(MongoDbPlugin.MONGO_PLUGIN_CONFIGURATION_PREFIX); + } + } + + if (application == null) { + throw new PluginException("Unable to find application plugin"); + } + + String[] clientNames = mongoConfiguration.getStringArray("clients"); + Set allDbNames = new HashSet(); + + if (clientNames == null || clientNames.length == 0) { + LOGGER.info("No Mongo client configured, MongoDB support disabled"); + return InitState.INITIALIZED; + } + + for (String clientName : clientNames) { + Configuration clientConfiguration = mongoConfiguration.subset("client." + clientName); + boolean async = clientConfiguration.getBoolean("async", false); + + if (async) { + AsyncHolder.INSTANCE.registerClient(clientName, clientConfiguration); + hasAsyncClients = true; + } else { + SyncHolder.INSTANCE.registerClient(clientName, clientConfiguration); + hasSyncClients = true; + } + + String[] dbNames = clientConfiguration.getStringArray("databases"); + + if (dbNames != null) { + for (String dbName : dbNames) { + String alias = clientConfiguration.getString(String.format("alias.%s", dbName), dbName); + + if (allDbNames.contains(alias)) { + throw SeedException.createNew(MongoDbErrorCodes.DUPLICATE_DATABASE_NAME) + .put("clientName", clientName) + .put("dbName", dbName); + } else { + allDbNames.add(alias); + } + + if (async) { + AsyncHolder.INSTANCE.registerDatabase(clientName, dbName, alias); + } else { + SyncHolder.INSTANCE.registerDatabase(clientName, dbName, alias); + } + } + } + } + + return InitState.INITIALIZED; + } + + @Override + public void stop() { + if (hasSyncClients) { + SyncHolder.INSTANCE.shutdown(); + } + + if (hasAsyncClients) { + AsyncHolder.INSTANCE.shutdown(); + } + } + + @Override + public Collection> requiredPlugins() { + Collection> plugins = new ArrayList>(); + plugins.add(ApplicationPlugin.class); + return plugins; + } + + @Override + public Object nativeUnitModule() { + return new AbstractModule() { + @Override + protected void configure() { + if (hasSyncClients) { + install(SyncHolder.INSTANCE.getModule()); + } + + if (hasAsyncClients) { + install(AsyncHolder.INSTANCE.getModule()); + } + } + }; + } +} diff --git a/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/SyncMongoDbManager.java b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/SyncMongoDbManager.java new file mode 100644 index 000000000..2a2dac1eb --- /dev/null +++ b/persistence-support/mongodb/src/main/java/org/seedstack/seed/persistence/mongodb/internal/SyncMongoDbManager.java @@ -0,0 +1,174 @@ +/** + * Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. + * + * This file is part of SeedStack, An enterprise-oriented full development stack. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.seedstack.seed.persistence.mongodb.internal; + +import com.mongodb.DBDecoderFactory; +import com.mongodb.DBEncoderFactory; +import com.mongodb.MongoClient; +import com.mongodb.MongoClientOptions; +import com.mongodb.MongoClientURI; +import com.mongodb.MongoCredential; +import com.mongodb.ReadPreference; +import com.mongodb.ServerAddress; +import com.mongodb.WriteConcern; +import com.mongodb.client.MongoDatabase; +import org.apache.commons.configuration.Configuration; +import org.bson.codecs.configuration.CodecRegistry; +import org.seedstack.seed.core.api.SeedException; +import org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes; + +import javax.net.SocketFactory; +import java.util.Iterator; +import java.util.List; + +class SyncMongoDbManager extends AbstractMongoDbManager { + @Override + protected MongoClient doCreateClient(Configuration clientConfiguration) { + if (clientConfiguration.containsKey("uri")) { + return createMongoClientFromURI(clientConfiguration); + } else { + return createMongoClient(clientConfiguration); + } + } + + @Override + protected MongoDatabase doCreateDatabase(MongoClient client, String dbName) { + return client.getDatabase(dbName); + } + + @Override + protected void doClose(MongoClient client) { + client.close(); + } + + private MongoClient createMongoClientFromURI(Configuration clientConfiguration) { + String uri = clientConfiguration.getString("uri"); + + if (uri == null || uri.isEmpty()) { + throw SeedException.createNew(MongoDbErrorCodes.MISSING_URI); + } + + MongoClientURI mongoClientURI; + MongoClientOptions mongoClientOptions = buildMongoClientOptions(clientConfiguration); + if (mongoClientOptions != null) { + mongoClientURI = new MongoClientURI(uri, MongoClientOptions.builder(mongoClientOptions)); + } else { + mongoClientURI = new MongoClientURI(uri); + } + + return new MongoClient(mongoClientURI); + } + + private MongoClient createMongoClient(Configuration clientConfiguration) { + List serverAddresses = buildServerAddresses(clientConfiguration.getStringArray("hosts")); + + if (serverAddresses.isEmpty()) { + throw SeedException.createNew(MongoDbErrorCodes.MISSING_HOSTS_CONFIGURATION); + } + + MongoClientOptions mongoClientOptions = buildMongoClientOptions(clientConfiguration); + List mongoCredentials = buildMongoCredentials(clientConfiguration); + if (mongoClientOptions == null) { + if (mongoCredentials.isEmpty()) { + if (serverAddresses.size() == 1) { + return new MongoClient(serverAddresses.get(0)); + } else { + return new MongoClient(serverAddresses); + } + } else { + if (serverAddresses.size() == 1) { + return new MongoClient(serverAddresses.get(0), mongoCredentials); + } else { + return new MongoClient(serverAddresses, mongoCredentials); + } + } + } else { + if (mongoCredentials.isEmpty()) { + if (serverAddresses.size() == 1) { + return new MongoClient(serverAddresses.get(0), mongoClientOptions); + } else { + return new MongoClient(serverAddresses, mongoClientOptions); + } + } else { + if (serverAddresses.size() == 1) { + return new MongoClient(serverAddresses.get(0), mongoCredentials, mongoClientOptions); + } else { + return new MongoClient(serverAddresses, mongoCredentials, mongoClientOptions); + } + } + } + } + + private MongoClientOptions buildMongoClientOptions(Configuration clientConfiguration) { + Iterator it = clientConfiguration.getKeys("option"); + MongoClientOptions.Builder builder = MongoClientOptions.builder(); + + while (it.hasNext()) { + String key = it.next(); + String option = key.substring(7); + + if ("description".equals(option)) { + builder.description(clientConfiguration.getString(key)); + } else if ("minConnectionsPerHost".equals(option)) { + builder.minConnectionsPerHost(clientConfiguration.getInt(key)); + } else if ("connectionsPerHost".equals(option)) { + builder.connectionsPerHost(clientConfiguration.getInt(key)); + } else if ("threadsAllowedToBlockForConnectionMultiplier".equals(option)) { + builder.threadsAllowedToBlockForConnectionMultiplier(clientConfiguration.getInt(key)); + } else if ("serverSelectionTimeout".equals(option)) { + builder.serverSelectionTimeout(clientConfiguration.getInt(key)); + } else if ("maxWaitTime".equals(option)) { + builder.maxWaitTime(clientConfiguration.getInt(key)); + } else if ("maxConnectionIdleTime".equals(option)) { + builder.maxConnectionIdleTime(clientConfiguration.getInt(key)); + } else if ("maxConnectionLifeTime".equals(option)) { + builder.maxConnectionLifeTime(clientConfiguration.getInt(key)); + } else if ("connectTimeout".equals(option)) { + builder.connectTimeout(clientConfiguration.getInt(key)); + } else if ("socketTimeout".equals(option)) { + builder.socketTimeout(clientConfiguration.getInt(key)); + } else if ("socketKeepAlive".equals(option)) { + builder.socketKeepAlive(clientConfiguration.getBoolean(key)); + } else if ("sslEnabled".equals(option)) { + builder.sslEnabled(clientConfiguration.getBoolean(key)); + } else if ("sslInvalidHostNameAllowed".equals(option)) { + builder.sslInvalidHostNameAllowed(clientConfiguration.getBoolean(key)); + } else if ("readPreference".equals(option)) { + builder.readPreference(ReadPreference.valueOf(clientConfiguration.getString(key))); + } else if ("writeConcern".equals(option)) { + builder.writeConcern(WriteConcern.valueOf(clientConfiguration.getString(key))); + } else if ("codecRegistry".equals(option)) { + builder.codecRegistry(instanceFromClassName(CodecRegistry.class, clientConfiguration.getString(key))); + } else if ("socketFactory".equals(option)) { + builder.socketFactory(instanceFromClassName(SocketFactory.class, clientConfiguration.getString(key))); + } else if ("cursorFinalizerEnabled".equals(option)) { + builder.cursorFinalizerEnabled(clientConfiguration.getBoolean(key)); + } else if ("alwaysUseMBeans".equals(option)) { + builder.alwaysUseMBeans(clientConfiguration.getBoolean(key)); + } else if ("dbDecoderFactory".equals(option)) { + builder.dbDecoderFactory(instanceFromClassName(DBDecoderFactory.class, clientConfiguration.getString(key))); + } else if ("dbEncoderFactory".equals(option)) { + builder.dbEncoderFactory(instanceFromClassName(DBEncoderFactory.class, clientConfiguration.getString(key))); + } else if ("heartbeatFrequency".equals(option)) { + builder.heartbeatFrequency(clientConfiguration.getInt(key)); + } else if ("minHeartbeatFrequency".equals(option)) { + builder.minHeartbeatFrequency(clientConfiguration.getInt(key)); + } else if ("heartbeatConnectTimeout".equals(option)) { + builder.heartbeatConnectTimeout(clientConfiguration.getInt(key)); + } else if ("heartbeatSocketTimeout".equals(option)) { + builder.heartbeatSocketTimeout(clientConfiguration.getInt(key)); + } else { + throw SeedException.createNew(MongoDbErrorCodes.UNKNOWN_CLIENT_OPTION).put("option", option); + } + } + + return builder.build(); + } +} diff --git a/persistence-support/mongodb/src/main/resources/META-INF/errors/org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes.properties b/persistence-support/mongodb/src/main/resources/META-INF/errors/org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes.properties new file mode 100644 index 000000000..0ab8e810a --- /dev/null +++ b/persistence-support/mongodb/src/main/resources/META-INF/errors/org.seedstack.seed.persistence.mongodb.api.MongoDbErrorCodes.properties @@ -0,0 +1,15 @@ +# +# Copyright (c) 2013-2015 by The SeedStack authors. All rights reserved. +# +# This file is part of SeedStack, An enterprise-oriented full development stack. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# + +UNABLE_TO_CREATE_CLIENT.message = Unable to create MongoDB client named "${clientName}" +UNKNOWN_DATABASE.message = Unable to find database "${dbName}" using client "${clientName}" +DUPLICATE_DATABASE_NAME.message = Duplicate database name "${dbName}" +DUPLICATE_DATABASE_NAME.fix = Consider changing the database name or defining an alias with the following configuration property: org.seedstack.seed.persistence.mongo.client.${clientName}.alias.${dbName} = your-alias-name +UNKNOWN_CLIENT_OPTION.message = Unknown client option "${option}" \ No newline at end of file diff --git a/persistence-support/mongodb/src/main/resources/META-INF/services/io.nuun.kernel.api.Plugin b/persistence-support/mongodb/src/main/resources/META-INF/services/io.nuun.kernel.api.Plugin new file mode 100644 index 000000000..6db54f6c0 --- /dev/null +++ b/persistence-support/mongodb/src/main/resources/META-INF/services/io.nuun.kernel.api.Plugin @@ -0,0 +1 @@ +org.seedstack.seed.persistence.mongodb.internal.MongoDbPlugin \ No newline at end of file diff --git a/persistence-support/mongodb/src/test/resources/logback-test.xml b/persistence-support/mongodb/src/test/resources/logback-test.xml new file mode 100644 index 000000000..7db11a54b --- /dev/null +++ b/persistence-support/mongodb/src/test/resources/logback-test.xml @@ -0,0 +1,22 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + \ No newline at end of file diff --git a/persistence-support/pom.xml b/persistence-support/pom.xml index 74d012eb3..770ff51e3 100644 --- a/persistence-support/pom.xml +++ b/persistence-support/pom.xml @@ -58,6 +58,7 @@ jpa jdbc neo4j + mongodb elasticsearch tinkerpop inmemory