| /** |
| * 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.apache.felix.useradmin.mongodb; |
| |
| import static org.apache.felix.useradmin.mongodb.MongoSerializerHelper.NAME; |
| import static org.apache.felix.useradmin.mongodb.MongoSerializerHelper.TYPE; |
| |
| import java.util.ArrayList; |
| import java.util.Dictionary; |
| import java.util.List; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| import org.apache.felix.useradmin.RoleRepositoryStore; |
| import org.osgi.framework.Filter; |
| import org.osgi.framework.FrameworkUtil; |
| import org.osgi.framework.InvalidSyntaxException; |
| import org.osgi.service.cm.ConfigurationException; |
| import org.osgi.service.cm.ManagedService; |
| import org.osgi.service.log.LogService; |
| import org.osgi.service.useradmin.Role; |
| import org.osgi.service.useradmin.UserAdminEvent; |
| import org.osgi.service.useradmin.UserAdminListener; |
| |
| import com.mongodb.BasicDBObject; |
| import com.mongodb.DBCollection; |
| import com.mongodb.DBCursor; |
| import com.mongodb.DBObject; |
| import com.mongodb.MongoException; |
| import com.mongodb.WriteResult; |
| |
| /** |
| * Provides a repository store that uses MongoDB for storing the role information. |
| * <p> |
| * This service can also be configured at runtime by using the PID {@value #PID}.<br/> |
| * The configuration options recognized by this service are: |
| * </p> |
| * <dl> |
| * <dt>server</dt> |
| * <dd>A space separated string containing the MongoDB servers. The format for this string is: "<code><host1:port1> <host2:port2></code>". This value is optional;</dd> |
| * <dt>dbname</dt> |
| * <dd>A string value containing the name of the database to use for this store. This value is optional;</dd> |
| * <dt>collection</dt> |
| * <dd>The name of the database collection to use for this store. This value is optional;</dd> |
| * <dt>username</dt> |
| * <dd>A string value representing the name of the user to authenticate against MongoDB. This value is optional;</dd> |
| * <dt>password</dt> |
| * <dd>A string value representing the password to authenticate against MongoDB. This value is optional.</dd> |
| * </dl> |
| * <p> |
| * Alternatively, one can also supply the above mentioned configuration keys prefixed with |
| * "<tt>org.apache.felix.useradmin.mongodb.</tt>" as system properties (e.g.: |
| * <tt>-Dorg.apache.felix.useradmin.mongodb.server=my.mongo.server:27017</tt>). However, this |
| * implies that only a single store can be configured on a system (which could be a sensible |
| * default for some situations)! |
| * </p> |
| * <p> |
| * By default, the following values are used: |
| * </p> |
| * <table> |
| * <tr><td><tt>server</tt></td><td>"<tt>localhost:27017</tt>"</td></tr> |
| * <tr><td><tt>dbname</tt></td><td>"<tt>ua_repo</tt>"</td></tr> |
| * <tr><td><tt>collection</tt></td><td>"<tt>useradmin</tt>"</td></tr> |
| * <tr><td><tt>username</tt></td><td><none></td></tr> |
| * <tr><td><tt>password</tt></td><td><none></td></tr> |
| * </table> |
| * <p> |
| * This class is thread-safe. |
| * </p> |
| */ |
| public class MongoDBStore implements RoleProvider, RoleRepositoryStore, UserAdminListener, ManagedService { |
| |
| /** The PID for the managed service reference. */ |
| public static final String PID = "org.apache.felix.useradmin.mongodb"; |
| |
| /** |
| * A space-separated array with server definitions to access MongoDB. |
| * Format = "<host1:port1> <host2:port2>". |
| * */ |
| private static final String KEY_MONGODB_SERVER = "server"; |
| /** The name of the MongoDB database instance. */ |
| private static final String KEY_MONGODB_DBNAME = "dbname"; |
| /** The username of the MongoDB database instance. */ |
| private static final String KEY_MONGODB_USERNAME = "username"; |
| /** The password of the MongoDB database instance. */ |
| private static final String KEY_MONGODB_PASSWORD = "password"; |
| /** The name of the MongoDB collection to use. */ |
| private static final String KEY_MONGODB_COLLECTION_NAME = "collection"; |
| |
| private static final String PREFIX = PID.concat("."); |
| /** Default MongoDB server; first checks a system property */ |
| private static final String DEFAULT_MONGODB_SERVER = System.getProperty(PREFIX.concat(KEY_MONGODB_SERVER), "localhost:27017"); |
| /** Default MongoDB name */ |
| private static final String DEFAULT_MONGODB_DBNAME = System.getProperty(PREFIX.concat(KEY_MONGODB_DBNAME), "ua_repo"); |
| /** Default MongoDB collection */ |
| private static final String DEFAULT_MONGODB_COLLECTION = System.getProperty(PREFIX.concat(KEY_MONGODB_COLLECTION_NAME), "useradmin"); |
| /** Default MongoDB username */ |
| private static final String DEFAULT_MONGODB_USERNAME = System.getProperty(PREFIX.concat(KEY_MONGODB_USERNAME)); |
| /** Default MongoDB password */ |
| private static final String DEFAULT_MONGODB_PASSWORD = System.getProperty(PREFIX.concat(KEY_MONGODB_PASSWORD)); |
| |
| private final AtomicReference<MongoDB> m_mongoDbRef; |
| private final MongoSerializerHelper m_helper; |
| |
| private volatile LogService m_log; |
| |
| /** |
| * Creates a new {@link MongoDBStore} instance. |
| */ |
| public MongoDBStore() { |
| m_mongoDbRef = new AtomicReference<MongoDB>(); |
| m_helper = new MongoSerializerHelper(this); |
| } |
| |
| @Override |
| public Role addRole(String roleName, int type) throws MongoException { |
| if (roleName == null) { |
| throw new IllegalArgumentException("Role cannot be null!"); |
| } |
| |
| DBCollection coll = getCollection(); |
| |
| Role role = getRole(roleName); |
| if (role != null) { |
| return null; |
| } |
| |
| // Role does not exist; insert it... |
| DBObject data = m_helper.serialize(roleName, type); |
| |
| WriteResult result = coll.insert(data); |
| |
| if (result.getLastError() != null) { |
| result.getLastError().throwOnError(); |
| } |
| |
| // FELIX-4400: ensure we return the correct role... |
| return getRole(roleName); |
| } |
| |
| /** |
| * Closes this store and disconnects from the MongoDB backend. |
| */ |
| public void close() { |
| MongoDB mongoDB = m_mongoDbRef.get(); |
| if (mongoDB != null) { |
| mongoDB.disconnect(); |
| } |
| m_mongoDbRef.set(null); |
| } |
| |
| @Override |
| public Role[] getRoles(String filterValue) throws InvalidSyntaxException, MongoException { |
| List<Role> roles = new ArrayList<Role>(); |
| |
| Filter filter = null; |
| if (filterValue != null) { |
| filter = FrameworkUtil.createFilter(filterValue); |
| } |
| |
| DBCollection coll = getCollection(); |
| |
| DBCursor cursor = coll.find(); |
| try { |
| while (cursor.hasNext()) { |
| // Hmm, there might be a more clever way of doing this... |
| Role role = m_helper.deserialize(cursor.next()); |
| if ((filter == null) || filter.match(role.getProperties())) { |
| roles.add(role); |
| } |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| return roles.toArray(new Role[roles.size()]); |
| } |
| |
| @Override |
| public Role getRole(String name) { |
| DBCollection coll = getCollection(); |
| |
| DBCursor cursor = coll.find(getTemplateObject(name)); |
| try { |
| if (cursor.hasNext()) { |
| return m_helper.deserialize(cursor.next()); |
| } |
| } finally { |
| cursor.close(); |
| } |
| |
| return null; |
| } |
| |
| @Override |
| public Role getRoleByName(String name) throws MongoException { |
| return getRole(name); |
| } |
| |
| @Override |
| public Role removeRole(String roleName) throws MongoException { |
| DBCollection coll = getCollection(); |
| |
| Role role = getRole(roleName); |
| if (role == null) { |
| return null; |
| } |
| |
| WriteResult result = coll.remove(getTemplateObject(role)); |
| |
| if (result.getLastError() != null) { |
| result.getLastError().throwOnError(); |
| } |
| |
| return role; |
| } |
| |
| @Override |
| public void roleChanged(UserAdminEvent event) { |
| if (UserAdminEvent.ROLE_CHANGED == event.getType()) { |
| // Only the changes are interesting, as the creation and |
| // removal are already caught by #addRole and #removeRole.... |
| Role changedRole = event.getRole(); |
| |
| try { |
| DBCollection coll = getCollection(); |
| |
| DBObject query = getTemplateObject(changedRole); |
| DBObject update = m_helper.serializeUpdate(changedRole); |
| |
| WriteResult result = coll.update(query, update, false /* upsert */, false /* multi */); |
| |
| if (result.getLastError() != null) { |
| result.getLastError().throwOnError(); |
| } |
| } |
| catch (MongoException e) { |
| m_log.log(LogService.LOG_WARNING, "Failed to update changed role: " + changedRole.getName(), e); |
| } |
| } |
| } |
| |
| /** |
| * @param log the log-service to set, cannot be <code>null</code>. |
| */ |
| public void setLogService(LogService log) { |
| m_log = log; |
| } |
| |
| @Override |
| public void updated(Dictionary properties) throws ConfigurationException { |
| // Defaults to "ua_repo" |
| String newDbName = getProperty(properties, KEY_MONGODB_DBNAME, DEFAULT_MONGODB_DBNAME); |
| // Defaults to "localhost:27017" |
| String newServers = getProperty(properties, KEY_MONGODB_SERVER, DEFAULT_MONGODB_SERVER); |
| // Defaults to "useradmin" |
| String newCollectionName = getProperty(properties, KEY_MONGODB_COLLECTION_NAME, DEFAULT_MONGODB_COLLECTION); |
| // Defaults to null |
| String newUsername = getProperty(properties, KEY_MONGODB_USERNAME, DEFAULT_MONGODB_USERNAME); |
| // Defaults to null. FELIX-3774; use correct property name... |
| String newPassword = getProperty(properties, KEY_MONGODB_PASSWORD, DEFAULT_MONGODB_PASSWORD); |
| |
| MongoDB newMongoDb = new MongoDB(newServers, newDbName, newCollectionName); |
| |
| MongoDB oldMongoDb; |
| do { |
| oldMongoDb = m_mongoDbRef.get(); |
| } |
| while (!m_mongoDbRef.compareAndSet(oldMongoDb, newMongoDb)); |
| |
| try { |
| // FELIX-3775: oldMongoDb can be null when supplying the configuration for the first time... |
| if (oldMongoDb != null) { |
| oldMongoDb.disconnect(); |
| } |
| } |
| catch (MongoException e) { |
| m_log.log(LogService.LOG_WARNING, "Failed to disconnect from (old) MongoDB!", e); |
| } |
| |
| try { |
| connectToDB(newMongoDb, newUsername, newPassword); |
| } |
| catch (MongoException e) { |
| m_log.log(LogService.LOG_WARNING, "Failed to connect to (new) MongoDB!", e); |
| throw new ConfigurationException(DEFAULT_MONGODB_USERNAME, "Failed to connect!", e); |
| } |
| } |
| |
| /** |
| * Creates a connection to MongoDB using the given credentials. |
| * |
| * @param mongoDB the {@link MongoDB} facade to connect to; |
| * @param userName the (optional) user name to use; |
| * @param password the (optional) password to use. |
| * @throws MongoException in case the connection or authentication failed. |
| */ |
| private void connectToDB(MongoDB mongoDB, String userName, String password) throws MongoException { |
| if (!mongoDB.connect(userName, password)) { |
| throw new MongoException("Failed to connect to MongoDB! Authentication failed!"); |
| } |
| |
| DBCollection collection = mongoDB.getCollection(); |
| if (collection == null) { |
| throw new MongoException("Failed to connect to MongoDB! No collection returned!"); |
| } |
| |
| collection.ensureIndex(new BasicDBObject(NAME, 1).append("unique", true)); |
| } |
| |
| /** |
| * Returns the current database collection. |
| * |
| * @return the database collection to work with, cannot be <code>null</code>. |
| * @throws MongoException in case no connection to MongoDB exists. |
| */ |
| private DBCollection getCollection() { |
| MongoDB mongoDB = m_mongoDbRef.get(); |
| if (mongoDB == null) { |
| throw new MongoException("No connection to MongoDB?!"); |
| } |
| return mongoDB.getCollection(); |
| } |
| |
| /** |
| * Returns the value for the given key from the given properties. |
| * |
| * @param properties the properties to get the value from, may be <code>null</code>; |
| * @param key the key to retrieve the value for, cannot be <code>null</code>; |
| * @param defaultValue the default value to use in case no value is present in the given dictionary, the value is not a string, or the dictionary itself was <code>null</code>. |
| * @return the value, can be <code>null</code> in case the given key lead to a null value, or a null value was supplied as default value. |
| */ |
| private String getProperty(Dictionary properties, String key, String defaultValue) { |
| String result = defaultValue; |
| if (properties != null) { |
| Object value = properties.get(key); |
| if (value != null && (value instanceof String)) { |
| result = (String) value; |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Creates a template object for the given role. |
| * |
| * @param role the role to create a template object for, cannot be <code>null</code>. |
| * @return a template object for MongoDB, never <code>null</code>. |
| */ |
| private DBObject getTemplateObject(Role role) { |
| BasicDBObject query = new BasicDBObject(); |
| query.put(NAME, role.getName()); |
| query.put(TYPE, role.getType()); |
| return query; |
| } |
| |
| /** |
| * Creates a template object for the given (role)name. |
| * |
| * @param name the name of the role to create a template object for, cannot be <code>null</code>. |
| * @return a template object for MongoDB, never <code>null</code>. |
| */ |
| private DBObject getTemplateObject(String name) { |
| BasicDBObject query = new BasicDBObject(); |
| query.put(NAME, name); |
| return query; |
| } |
| } |