001/*
002 * (C) Copyright 2014-2017 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 *     Kevin Leturc
019 *     Funsho David
020 */
021package org.nuxeo.runtime.mongodb;
022
023import java.io.IOException;
024import java.io.InputStream;
025import java.nio.file.Files;
026import java.nio.file.Paths;
027import java.security.GeneralSecurityException;
028import java.security.KeyStore;
029import java.util.stream.StreamSupport;
030
031import javax.net.ssl.SSLContext;
032
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.apache.http.ssl.SSLContextBuilder;
037import org.apache.http.ssl.SSLContexts;
038
039import com.mongodb.MongoClient;
040import com.mongodb.MongoClientOptions;
041import com.mongodb.MongoClientURI;
042import com.mongodb.ServerAddress;
043import com.mongodb.client.MongoDatabase;
044import com.mongodb.client.MongoIterable;
045
046/**
047 * Helper for connection to the MongoDB server
048 *
049 * @since 9.1
050 */
051public class MongoDBConnectionHelper {
052
053    private static final Log log = LogFactory.getLog(MongoDBConnectionHelper.class);
054
055    private static final String DB_DEFAULT = "nuxeo";
056
057    private static final int MONGODB_OPTION_CONNECTION_TIMEOUT_MS = 30000;
058
059    private static final int MONGODB_OPTION_SOCKET_TIMEOUT_MS = 60000;
060
061    private MongoDBConnectionHelper() {
062        // Empty
063    }
064
065    /**
066     * Initialize a connection to the MongoDB server
067     *
068     * @param server the server url
069     * @return the MongoDB client
070     */
071    public static MongoClient newMongoClient(String server) {
072        MongoDBConnectionConfig config = new MongoDBConnectionConfig();
073        config.server = server;
074        return newMongoClient(config);
075    }
076
077    /**
078     * Initializes a connection to the MongoDB server.
079     *
080     * @param config the MongoDB connection config
081     * @return the MongoDB client
082     * @since 10.3
083     */
084    public static MongoClient newMongoClient(MongoDBConnectionConfig config) {
085        String server = config.server;
086        if (StringUtils.isBlank(server)) {
087            throw new RuntimeException("Missing <server> in MongoDB descriptor");
088        }
089        MongoClientOptions.Builder optionsBuilder = MongoClientOptions.builder()
090                  // can help to prevent firewall disconnecting inactive connection, option not available from URI
091                  .socketKeepAlive(true)
092                  // don't wait forever by default, can be overridden using URI options
093                  .connectTimeout(MONGODB_OPTION_CONNECTION_TIMEOUT_MS)
094                  .socketTimeout(MONGODB_OPTION_SOCKET_TIMEOUT_MS)
095                  .description("Nuxeo");
096        SSLContext sslContext = getSSLContext(config);
097        if (sslContext == null) {
098            if (config.ssl != null) {
099                optionsBuilder.sslEnabled(config.ssl.booleanValue());
100            }
101        } else {
102            optionsBuilder.sslEnabled(true);
103            optionsBuilder.sslContext(sslContext);
104        }
105        MongoClient client;
106        if (server.startsWith("mongodb://")) {
107            // allow mongodb:// URI syntax for the server, to pass everything in one string
108            client = new MongoClient(new MongoClientURI(server, optionsBuilder));
109        } else {
110            client = new MongoClient(new ServerAddress(server), optionsBuilder.build());
111        }
112        if (log.isDebugEnabled()) {
113            log.debug("MongoClient initialized with options: " + client.getMongoClientOptions().toString());
114        }
115        return client;
116    }
117
118    protected static SSLContext getSSLContext(MongoDBConnectionConfig config) {
119        try {
120            KeyStore trustStore = loadKeyStore(config.trustStorePath, config.trustStorePassword, config.trustStoreType);
121            KeyStore keyStore = loadKeyStore(config.keyStorePath, config.keyStorePassword, config.keyStoreType);
122            if (trustStore == null && keyStore == null) {
123                return null;
124            }
125            SSLContextBuilder sslContextBuilder = SSLContexts.custom();
126            if (trustStore != null) {
127                sslContextBuilder.loadTrustMaterial(trustStore, null);
128            }
129            if (keyStore != null) {
130                sslContextBuilder.loadKeyMaterial(keyStore, null);
131            }
132            return sslContextBuilder.build();
133        } catch (GeneralSecurityException | IOException e) {
134            throw new RuntimeException("Cannot setup SSL context: " + config, e);
135        }
136    }
137
138    protected static KeyStore loadKeyStore(String path, String password, String type)
139            throws GeneralSecurityException, IOException {
140        if (StringUtils.isBlank(path)) {
141            return null;
142        }
143        String keyStoreType = StringUtils.defaultIfBlank(type, KeyStore.getDefaultType());
144        KeyStore keyStore = KeyStore.getInstance(keyStoreType);
145        char[] passwordChars = StringUtils.isBlank(password) ? null : password.toCharArray();
146        try (InputStream is = Files.newInputStream(Paths.get(path))) {
147            keyStore.load(is, passwordChars);
148        }
149        return keyStore;
150    }
151
152    /**
153     * @return a database representing the specified database
154     */
155    public static MongoDatabase getDatabase(MongoClient mongoClient, String dbname) {
156        if (StringUtils.isBlank(dbname)) {
157            dbname = DB_DEFAULT;
158        }
159        return mongoClient.getDatabase(dbname);
160    }
161
162    /**
163     * Check if the collection exists and if it is not empty
164     *
165     * @param mongoDatabase the Mongo database
166     * @param collection the collection name
167     * @return true if the collection exists and not empty, false otherwise
168     */
169    public static boolean hasCollection(MongoDatabase mongoDatabase, String collection) {
170        MongoIterable<String> collections = mongoDatabase.listCollectionNames();
171        boolean found = StreamSupport.stream(collections.spliterator(), false).anyMatch(collection::equals);
172        return found && mongoDatabase.getCollection(collection).count() > 0;
173    }
174}