001/* 002 * (C) Copyright 2014-2020 Nuxeo (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 static java.util.concurrent.TimeUnit.MILLISECONDS; 024 025import java.io.IOException; 026import java.io.InputStream; 027import java.nio.file.Files; 028import java.nio.file.Paths; 029import java.security.GeneralSecurityException; 030import java.security.KeyStore; 031import java.util.List; 032import java.util.function.Consumer; 033import java.util.stream.StreamSupport; 034 035import javax.net.ssl.SSLContext; 036 037import org.apache.commons.beanutils.BeanUtilsBean; 038import org.apache.commons.beanutils.ConvertUtilsBean; 039import org.apache.commons.beanutils.Converter; 040import org.apache.commons.beanutils.FluentPropertyBeanIntrospector; 041import org.apache.commons.beanutils.PropertyUtilsBean; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.http.ssl.SSLContextBuilder; 044import org.apache.http.ssl.SSLContexts; 045import org.apache.logging.log4j.LogManager; 046import org.apache.logging.log4j.Logger; 047import org.nuxeo.runtime.RuntimeServiceException; 048 049import com.mongodb.ConnectionString; 050import com.mongodb.MongoClientSettings; 051import com.mongodb.ReadConcern; 052import com.mongodb.ReadConcernLevel; 053import com.mongodb.ReadPreference; 054import com.mongodb.ServerAddress; 055import com.mongodb.WriteConcern; 056import com.mongodb.client.MongoClient; 057import com.mongodb.client.MongoClients; 058import com.mongodb.client.MongoDatabase; 059import com.mongodb.client.MongoIterable; 060 061/** 062 * Helper for connection to the MongoDB server 063 * 064 * @since 9.1 065 */ 066public class MongoDBConnectionHelper { 067 068 private static final Logger log = LogManager.getLogger(MongoDBConnectionHelper.class); 069 070 private static final String DB_DEFAULT = "nuxeo"; 071 072 private static final int MONGODB_OPTION_CONNECT_TIMEOUT_MS = 30000; 073 074 private static final int MONGODB_OPTION_READ_TIMEOUT_MS = 60000; 075 076 /** @since 11.1 */ 077 public static class ReadPreferenceConverter implements Converter { 078 079 public static final ReadPreferenceConverter INSTANCE = new ReadPreferenceConverter(); 080 081 @SuppressWarnings("unchecked") 082 @Override 083 public <T> T convert(Class<T> type, Object value) { 084 return (T) ReadPreference.valueOf((String) value); 085 } 086 } 087 088 /** @since 11.1 */ 089 public static class ReadConcernConverter implements Converter { 090 091 public static final ReadConcernConverter INSTANCE = new ReadConcernConverter(); 092 093 @SuppressWarnings("unchecked") 094 @Override 095 public <T> T convert(Class<T> type, Object value) { 096 ReadConcern readConcern; 097 if ("default".equalsIgnoreCase((String) value)) { 098 readConcern = ReadConcern.DEFAULT; 099 } else { 100 ReadConcernLevel level = ReadConcernLevel.fromString((String) value); 101 readConcern = new ReadConcern(level); 102 } 103 return (T) readConcern; 104 } 105 } 106 107 /** @since 11.1 */ 108 public static class WriteConcernConverter implements Converter { 109 110 public static final WriteConcernConverter INSTANCE = new WriteConcernConverter(); 111 112 @SuppressWarnings("unchecked") 113 @Override 114 public <T> T convert(Class<T> type, Object value) { 115 return (T) WriteConcern.valueOf((String) value); 116 } 117 } 118 119 private MongoDBConnectionHelper() { 120 // Empty 121 } 122 123 /** 124 * Initialize a connection to the MongoDB server 125 * 126 * @param server the server url 127 * @return the MongoDB client 128 */ 129 public static MongoClient newMongoClient(String server) { 130 MongoDBConnectionConfig config = new MongoDBConnectionConfig(); 131 config.server = server; 132 return newMongoClient(config); 133 } 134 135 /** 136 * Initializes a connection to the MongoDB server. 137 * 138 * @param config the MongoDB connection config 139 * @return the MongoDB client 140 * @since 10.3 141 */ 142 public static MongoClient newMongoClient(MongoDBConnectionConfig config) { 143 return newMongoClient(config, null); 144 } 145 146 /** 147 * Initializes a connection to the MongoDB server. 148 * 149 * @param config the MongoDB connection config 150 * @param settingsConsumer a consumer of the client settings builder 151 * @return the MongoDB client 152 * @since 11.1 153 */ 154 public static MongoClient newMongoClient(MongoDBConnectionConfig config, 155 Consumer<MongoClientSettings.Builder> settingsConsumer) { 156 String server = config.server; 157 if (StringUtils.isBlank(server)) { 158 throw new RuntimeServiceException("Missing <server> in MongoDB descriptor"); 159 } 160 MongoClientSettings.Builder settingsBuilder = MongoClientSettings.builder().applicationName("Nuxeo"); 161 SSLContext sslContext = getSSLContext(config); 162 if (sslContext == null) { 163 if (config.ssl != null) { 164 settingsBuilder.applyToSslSettings(s -> s.enabled(config.ssl.booleanValue())); 165 } 166 } else { 167 settingsBuilder.applyToSslSettings(s -> s.enabled(true).context(sslContext)); 168 } 169 170 // don't wait forever by default when connecting 171 settingsBuilder.applyToSocketSettings(s -> s.connectTimeout(MONGODB_OPTION_CONNECT_TIMEOUT_MS, MILLISECONDS) 172 .readTimeout(MONGODB_OPTION_READ_TIMEOUT_MS, MILLISECONDS)); 173 174 // set properties from Nuxeo config descriptor 175 populateProperties(config, settingsBuilder); 176 177 // hook for caller to set additional properties 178 if (settingsConsumer != null) { 179 settingsConsumer.accept(settingsBuilder); 180 } 181 182 if (server.startsWith("mongodb://") || server.startsWith("mongodb+srv://")) { 183 // allow mongodb*:// URI syntax for the server, to pass everything in one string 184 settingsBuilder.applyConnectionString(new ConnectionString(server)); 185 } else { 186 settingsBuilder.applyToClusterSettings(b -> b.hosts(List.of(new ServerAddress(server)))); 187 } 188 MongoClientSettings settings = settingsBuilder.build(); 189 MongoClient client = MongoClients.create(settings); 190 log.debug("MongoClient initialized with settings: {}", settings); 191 return client; 192 } 193 194 /** 195 * Exists to be tested. 196 * 197 * @since 11.4 198 */ 199 protected static void populateProperties(MongoDBConnectionConfig config, MongoClientSettings.Builder settingsBuilder) { 200 ConvertUtilsBean convertUtils = new ConvertUtilsBean(); 201 convertUtils.register(ReadPreferenceConverter.INSTANCE, ReadPreference.class); 202 convertUtils.register(ReadConcernConverter.INSTANCE, ReadConcern.class); 203 convertUtils.register(WriteConcernConverter.INSTANCE, WriteConcern.class); 204 PropertyUtilsBean propertyUtils = new PropertyUtilsBean(); 205 propertyUtils.addBeanIntrospector(new FluentPropertyBeanIntrospector("")); 206 BeanUtilsBean beanUtils = new BeanUtilsBean(convertUtils, propertyUtils); 207 try { 208 beanUtils.populate(settingsBuilder, config.properties); 209 } catch (ReflectiveOperationException e) { 210 throw new RuntimeServiceException(e); 211 } 212 } 213 214 protected static SSLContext getSSLContext(MongoDBConnectionConfig config) { 215 try { 216 KeyStore trustStore = loadKeyStore(config.trustStorePath, config.trustStorePassword, config.trustStoreType); 217 KeyStore keyStore = loadKeyStore(config.keyStorePath, config.keyStorePassword, config.keyStoreType); 218 if (trustStore == null && keyStore == null) { 219 return null; 220 } 221 SSLContextBuilder sslContextBuilder = SSLContexts.custom(); 222 if (trustStore != null) { 223 sslContextBuilder.loadTrustMaterial(trustStore, null); 224 } 225 if (keyStore != null) { 226 sslContextBuilder.loadKeyMaterial(keyStore, 227 StringUtils.isBlank(config.keyStorePassword) ? null : config.keyStorePassword.toCharArray()); 228 } 229 return sslContextBuilder.build(); 230 } catch (GeneralSecurityException | IOException e) { 231 throw new RuntimeServiceException("Cannot setup SSL context: " + config, e); 232 } 233 } 234 235 protected static KeyStore loadKeyStore(String path, String password, String type) 236 throws GeneralSecurityException, IOException { 237 if (StringUtils.isBlank(path)) { 238 return null; 239 } 240 String keyStoreType = StringUtils.defaultIfBlank(type, KeyStore.getDefaultType()); 241 KeyStore keyStore = KeyStore.getInstance(keyStoreType); 242 char[] passwordChars = StringUtils.isBlank(password) ? null : password.toCharArray(); 243 try (InputStream is = Files.newInputStream(Paths.get(path))) { 244 keyStore.load(is, passwordChars); 245 } 246 return keyStore; 247 } 248 249 /** 250 * @return a database representing the specified database 251 */ 252 public static MongoDatabase getDatabase(MongoClient mongoClient, String dbname) { 253 if (StringUtils.isBlank(dbname)) { 254 dbname = DB_DEFAULT; 255 } 256 return mongoClient.getDatabase(dbname); 257 } 258 259 /** 260 * Check if the collection exists and if it is not empty 261 * 262 * @param mongoDatabase the Mongo database 263 * @param collection the collection name 264 * @return true if the collection exists and not empty, false otherwise 265 */ 266 public static boolean hasCollection(MongoDatabase mongoDatabase, String collection) { 267 MongoIterable<String> collections = mongoDatabase.listCollectionNames(); 268 boolean found = StreamSupport.stream(collections.spliterator(), false).anyMatch(collection::equals); 269 return found && mongoDatabase.getCollection(collection).estimatedDocumentCount() > 0; 270 } 271}