001/* 002 * (C) Copyright 2017 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 * Funsho David 018 * 019 */ 020 021package org.nuxeo.directory.mongodb; 022 023import static org.nuxeo.directory.mongodb.MongoDBSerializationHelper.MONGODB_ID; 024import static org.nuxeo.directory.mongodb.MongoDBSerializationHelper.MONGODB_SEQ; 025 026import java.io.Serializable; 027import java.util.ArrayList; 028import java.util.Calendar; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.LinkedList; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035import java.util.regex.Pattern; 036import java.util.stream.Collectors; 037 038import org.apache.commons.lang.StringUtils; 039import org.bson.Document; 040import org.bson.conversions.Bson; 041import org.nuxeo.ecm.core.api.DocumentModel; 042import org.nuxeo.ecm.core.api.DocumentModelList; 043import org.nuxeo.ecm.core.api.PropertyException; 044import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl; 045import org.nuxeo.ecm.core.api.model.Property; 046import org.nuxeo.ecm.core.api.security.SecurityConstants; 047import org.nuxeo.ecm.core.schema.types.Field; 048import org.nuxeo.ecm.core.schema.types.Type; 049import org.nuxeo.ecm.core.schema.types.primitives.IntegerType; 050import org.nuxeo.ecm.core.schema.types.primitives.LongType; 051import org.nuxeo.ecm.core.schema.types.primitives.StringType; 052import org.nuxeo.ecm.directory.BaseSession; 053import org.nuxeo.ecm.directory.DirectoryException; 054import org.nuxeo.ecm.directory.PasswordHelper; 055import org.nuxeo.ecm.directory.Reference; 056import org.nuxeo.ecm.directory.Session; 057import org.nuxeo.runtime.api.Framework; 058import org.nuxeo.runtime.mongodb.MongoDBConnectionHelper; 059import org.nuxeo.runtime.mongodb.MongoDBConnectionService; 060 061import com.mongodb.MongoWriteException; 062import com.mongodb.client.FindIterable; 063import com.mongodb.client.MongoCollection; 064import com.mongodb.client.MongoDatabase; 065import com.mongodb.client.model.FindOneAndUpdateOptions; 066import com.mongodb.client.model.ReturnDocument; 067import com.mongodb.client.model.Updates; 068import com.mongodb.client.result.DeleteResult; 069import com.mongodb.client.result.UpdateResult; 070 071/** 072 * MongoDB implementation of a {@link Session} 073 * 074 * @since 9.1 075 */ 076public class MongoDBSession extends BaseSession { 077 078 /** 079 * Prefix used to retrieve a MongoDB connection from {@link MongoDBConnectionService}. 080 * <p /> 081 * The connection id will be {@code directory/[DIRECTORY_NAME]}. 082 */ 083 public static final String DIRECTORY_CONNECTION_PREFIX = "directory/"; 084 085 protected final MongoDatabase database; 086 087 protected String countersCollectionName; 088 089 public MongoDBSession(MongoDBDirectory directory) { 090 super(directory, MongoDBReference.class); 091 MongoDBConnectionService mongoService = Framework.getService(MongoDBConnectionService.class); 092 database = mongoService.getDatabase(DIRECTORY_CONNECTION_PREFIX + directory.getDescriptor().name); 093 countersCollectionName = directory.getCountersCollectionName(); 094 } 095 096 @Override 097 public MongoDBDirectory getDirectory() { 098 return (MongoDBDirectory) directory; 099 } 100 101 @Override 102 protected DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) { 103 // Make a copy of fieldMap to avoid modifying it 104 fieldMap = new HashMap<>(fieldMap); 105 106 // Filter out reference fields for creation as we keep it in a different collection 107 Map<String, Object> newDocMap = fieldMap.entrySet() 108 .stream() 109 .filter(entry -> getDirectory().getReferences(entry.getKey()) == null) 110 .collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()), 111 HashMap::putAll); 112 Map<String, Field> schemaFieldMap = directory.getSchemaFieldMap(); 113 String idFieldName = schemaFieldMap.get(getIdField()).getName().getPrefixedName(); 114 String id; 115 if (autoincrementId) { 116 Document filter = MongoDBSerializationHelper.fieldMapToBson(MONGODB_ID, directoryName); 117 Bson update = Updates.inc(MONGODB_SEQ, 1L); 118 FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER); 119 Long longId = getCollection(countersCollectionName).findOneAndUpdate(filter, update, options) 120 .getLong(MONGODB_SEQ); 121 fieldMap.put(idFieldName, longId); 122 newDocMap.put(idFieldName, longId); 123 id = String.valueOf(longId); 124 } else { 125 id = String.valueOf(fieldMap.get(idFieldName)); 126 if (hasEntry(id)) { 127 throw new DirectoryException(String.format("Entry with id %s already exists", id)); 128 } 129 } 130 try { 131 132 for (Map.Entry<String, Field> entry : schemaFieldMap.entrySet()) { 133 Field field = entry.getValue(); 134 if (field != null) { 135 String fieldName = field.getName().getPrefixedName(); 136 Object value = newDocMap.get(fieldName); 137 Type type = field.getType(); 138 if (value instanceof String) { 139 String v = (String) value; 140 if (type instanceof IntegerType) { 141 newDocMap.put(fieldName, Integer.valueOf(v)); 142 } else if (type instanceof LongType) { 143 newDocMap.put(fieldName, Long.valueOf(v)); 144 } 145 } else if (value instanceof Number) { 146 if (type instanceof LongType && value instanceof Integer) { 147 newDocMap.put(fieldName, Long.valueOf((Integer) value)); 148 } else if (type instanceof StringType) { 149 newDocMap.put(fieldName, value.toString()); 150 } 151 } 152 // Load default values if defined and not present in the map 153 if (!newDocMap.containsKey(fieldName)) { 154 Object defaultValue = field.getDefaultValue(); 155 if (defaultValue != null) { 156 newDocMap.put(fieldName, defaultValue); 157 } 158 } 159 } 160 } 161 Document bson = MongoDBSerializationHelper.fieldMapToBson(newDocMap); 162 String password = (String) newDocMap.get(getPasswordField()); 163 if (password != null && !PasswordHelper.isHashed(password)) { 164 password = PasswordHelper.hashPassword(password, passwordHashAlgorithm); 165 bson.append(getPasswordField(), password); 166 } 167 getCollection().insertOne(bson); 168 } catch (MongoWriteException e) { 169 throw new DirectoryException(e); 170 } 171 return createEntryModel(null, schemaName, id, fieldMap, isReadOnly()); 172 } 173 174 @Override 175 protected List<String> updateEntryWithoutReferences(DocumentModel docModel) throws DirectoryException { 176 Map<String, Object> fieldMap = new HashMap<>(); 177 List<String> referenceFieldList = new LinkedList<>(); 178 Map<String, Field> schemaFieldMap = directory.getSchemaFieldMap(); 179 for (String fieldName : schemaFieldMap.keySet()) { 180 if (fieldName.equals(getIdField())) { 181 continue; 182 } 183 Property prop = docModel.getPropertyObject(schemaName, fieldName); 184 if (prop == null || !prop.isDirty() 185 || (fieldName.equals(getPasswordField()) && StringUtils.isEmpty((String) prop.getValue()))) { 186 continue; 187 } 188 if (getDirectory().isReference(fieldName)) { 189 referenceFieldList.add(fieldName); 190 } else { 191 Serializable value = prop.getValue(); 192 if (fieldName.equals(getPasswordField())) { 193 value = PasswordHelper.hashPassword((String) value, passwordHashAlgorithm); 194 } 195 if (value instanceof Calendar) { 196 value = ((Calendar) value).getTime(); 197 } 198 fieldMap.put(prop.getName(), value); 199 } 200 } 201 202 String idFieldName = schemaFieldMap.get(getIdField()).getName().getPrefixedName(); 203 String id = docModel.getId(); 204 Document bson = MongoDBSerializationHelper.fieldMapToBson(idFieldName, autoincrementId ? Long.valueOf(id) : id); 205 206 List<Bson> updates = fieldMap.entrySet().stream().map(e -> Updates.set(e.getKey(), e.getValue())).collect( 207 Collectors.toList()); 208 209 if (!updates.isEmpty()) { 210 try { 211 UpdateResult result = getCollection().updateOne(bson, Updates.combine(updates)); 212 // Throw an error if no document matched the update 213 if (!result.wasAcknowledged()) { 214 throw new DirectoryException( 215 "Error while updating the entry, the request has not been acknowledged by the server"); 216 } 217 if (result.getMatchedCount() == 0) { 218 throw new DirectoryException( 219 String.format("Error while updating the entry, no document was found with the id %s", id)); 220 } 221 } catch (MongoWriteException e) { 222 throw new DirectoryException(e); 223 } 224 } 225 return referenceFieldList; 226 } 227 228 @Override 229 public void deleteEntryWithoutReferences(String id) throws DirectoryException { 230 try { 231 String idFieldName = directory.getSchemaFieldMap().get(getIdField()).getName().getPrefixedName(); 232 DeleteResult result = getCollection().deleteOne( 233 MongoDBSerializationHelper.fieldMapToBson(idFieldName, autoincrementId ? Long.valueOf(id) : id)); 234 if (!result.wasAcknowledged()) { 235 throw new DirectoryException( 236 "Error while deleting the entry, the request has not been acknowledged by the server"); 237 } 238 } catch (MongoWriteException e) { 239 throw new DirectoryException(e); 240 } 241 } 242 243 @Override 244 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy, 245 boolean fetchReferences) throws DirectoryException { 246 return query(filter, fulltext, orderBy, fetchReferences, -1, 0); 247 } 248 249 @Override 250 public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy, 251 boolean fetchReferences, int limit, int offset) throws DirectoryException { 252 253 if (!hasPermission(SecurityConstants.READ)) { 254 return new DocumentModelListImpl(); 255 } 256 257 // Remove password as it is not possible to do queries with it 258 filter.remove(getPasswordField()); 259 Document bson = buildQuery(filter, fulltext); 260 261 DocumentModelList entries = new DocumentModelListImpl(); 262 263 FindIterable<Document> results = getCollection().find(bson).skip(offset); 264 if (limit > 0) { 265 results.limit(limit); 266 } 267 for (Document resultDoc : results) { 268 269 // Cast object to document model 270 Map<String, Object> fieldMap = MongoDBSerializationHelper.bsonToFieldMap(resultDoc); 271 // Remove password from results 272 if (!readAllColumns) { 273 fieldMap.remove(getPasswordField()); 274 } 275 DocumentModel doc = fieldMapToDocumentModel(fieldMap); 276 277 if (fetchReferences) { 278 Map<String, List<String>> targetIdsMap = new HashMap<>(); 279 for (Reference reference : directory.getReferences()) { 280 List<String> targetIds; 281 if (reference instanceof MongoDBReference) { 282 MongoDBReference mongoReference = (MongoDBReference) reference; 283 targetIds = mongoReference.getTargetIdsForSource(doc.getId(), this); 284 } else { 285 targetIds = reference.getTargetIdsForSource(doc.getId()); 286 } 287 targetIds = new ArrayList<>(targetIds); 288 Collections.sort(targetIds); 289 String fieldName = reference.getFieldName(); 290 targetIdsMap.computeIfAbsent(fieldName, key -> new ArrayList<>()).addAll(targetIds); 291 } 292 for (Map.Entry<String, List<String>> entry : targetIdsMap.entrySet()) { 293 String fieldName = entry.getKey(); 294 List<String> targetIds = entry.getValue(); 295 try { 296 doc.setProperty(schemaName, fieldName, targetIds); 297 } catch (PropertyException e) { 298 throw new DirectoryException(e); 299 } 300 } 301 } 302 entries.add(doc); 303 } 304 305 if (orderBy != null && !orderBy.isEmpty()) { 306 getDirectory().orderEntries(entries, orderBy); 307 } 308 309 return entries; 310 } 311 312 protected Document buildQuery(Map<String, Serializable> fieldMap, Set<String> fulltext) { 313 Map<String, Field> schemaFieldMap = directory.getSchemaFieldMap(); 314 Document bson = new Document(); 315 for (Map.Entry<String, Serializable> entry : fieldMap.entrySet()) { 316 Field field = schemaFieldMap.entrySet() 317 .stream() 318 .filter(e -> e.getValue().getName().getPrefixedName().equals(entry.getKey())) 319 .map(Map.Entry::getValue) 320 .findFirst() 321 .orElse(null); 322 323 Serializable v = entry.getValue(); 324 Object value = (field != null) ? MongoDBSerializationHelper.valueToBson(v, field.getType()) 325 : MongoDBSerializationHelper.valueToBson(v); 326 String key = entry.getKey(); 327 if (fulltext != null && fulltext.contains(key)) { 328 String val = String.valueOf(value); 329 if (val != null) { 330 val = val.replaceAll("%+", ".*"); 331 } 332 switch (substringMatchType) { 333 case subany: 334 addField(bson, key, Pattern.compile(val, Pattern.CASE_INSENSITIVE)); 335 break; 336 case subinitial: 337 addField(bson, key, Pattern.compile('^' + val, Pattern.CASE_INSENSITIVE)); 338 break; 339 case subfinal: 340 addField(bson, key, Pattern.compile(val + '$', Pattern.CASE_INSENSITIVE)); 341 break; 342 } 343 } else { 344 addField(bson, key, value); 345 } 346 } 347 return bson; 348 } 349 350 protected void addField(Document bson, String key, Object value) { 351 String keyFieldName = key; 352 Field field = directory.getSchemaFieldMap().get(key); 353 if (field != null) { 354 keyFieldName = field.getName().getPrefixedName(); 355 } 356 bson.put(keyFieldName, value); 357 } 358 359 @Override 360 public void close() throws DirectoryException { 361 getDirectory().removeSession(this); 362 } 363 364 @Override 365 public boolean authenticate(String username, String password) throws DirectoryException { 366 Document user = getCollection().find(MongoDBSerializationHelper.fieldMapToBson(getIdField(), username)).first(); 367 if (user == null) { 368 return false; 369 } 370 String storedPassword = user.getString(getPasswordField()); 371 return PasswordHelper.verifyPassword(password, storedPassword); 372 } 373 374 @Override 375 public boolean isAuthenticating() { 376 return directory.getSchemaFieldMap().containsKey(getPasswordField()); 377 } 378 379 @Override 380 public boolean hasEntry(String id) { 381 return getCollection().count(MongoDBSerializationHelper.fieldMapToBson(getIdField(), id)) > 0; 382 } 383 384 /** 385 * Retrieve a collection 386 * 387 * @param collection the collection name 388 * @return the MongoDB collection 389 */ 390 public MongoCollection<Document> getCollection(String collection) { 391 return database.getCollection(collection); 392 } 393 394 /** 395 * Retrieve the collection associated to this directory 396 * 397 * @return the MongoDB collection 398 */ 399 public MongoCollection<Document> getCollection() { 400 return getCollection(directoryName); 401 } 402 403 /** 404 * Check if the MongoDB server has the collection 405 * 406 * @param collection the collection name 407 * @return true if the server has the collection, false otherwise 408 */ 409 public boolean hasCollection(String collection) { 410 return MongoDBConnectionHelper.hasCollection(database, collection); 411 } 412 413 protected DocumentModel fieldMapToDocumentModel(Map<String, Object> fieldMap) { 414 String idFieldName = directory.getSchemaFieldMap().get(getIdField()).getName().getPrefixedName(); 415 if (!fieldMap.containsKey(idFieldName)) { 416 idFieldName = getIdField(); 417 } 418 String id = String.valueOf(fieldMap.get(idFieldName)); 419 return createEntryModel(null, schemaName, id, fieldMap, isReadOnly()); 420 } 421 422}