001/*
002 * (C) Copyright 2017-2019 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 javax.servlet.http.HttpServletResponse.SC_CONFLICT;
024import static org.apache.commons.lang3.StringUtils.isBlank;
025import static org.nuxeo.directory.mongodb.MongoDBSerializationHelper.MONGODB_ID;
026import static org.nuxeo.directory.mongodb.MongoDBSerializationHelper.MONGODB_SEQ;
027
028import java.io.Serializable;
029import java.util.ArrayList;
030import java.util.Calendar;
031import java.util.Collections;
032import java.util.HashMap;
033import java.util.LinkedList;
034import java.util.List;
035import java.util.Map;
036import java.util.Map.Entry;
037import java.util.Set;
038import java.util.regex.Pattern;
039import java.util.stream.Collectors;
040
041import org.apache.commons.lang3.StringUtils;
042import org.bson.Document;
043import org.bson.conversions.Bson;
044import org.nuxeo.ecm.core.api.DocumentModel;
045import org.nuxeo.ecm.core.api.DocumentModelList;
046import org.nuxeo.ecm.core.api.PropertyException;
047import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
048import org.nuxeo.ecm.core.api.model.Property;
049import org.nuxeo.ecm.core.api.security.SecurityConstants;
050import org.nuxeo.ecm.core.query.QueryParseException;
051import org.nuxeo.ecm.core.query.sql.model.Expression;
052import org.nuxeo.ecm.core.query.sql.model.OrderByExpr;
053import org.nuxeo.ecm.core.query.sql.model.OrderByList;
054import org.nuxeo.ecm.core.query.sql.model.QueryBuilder;
055import org.nuxeo.ecm.core.schema.types.Field;
056import org.nuxeo.ecm.core.schema.types.Type;
057import org.nuxeo.ecm.core.schema.types.primitives.IntegerType;
058import org.nuxeo.ecm.core.schema.types.primitives.LongType;
059import org.nuxeo.ecm.core.schema.types.primitives.StringType;
060import org.nuxeo.ecm.core.storage.State;
061import org.nuxeo.ecm.core.storage.mongodb.MongoDBAbstractQueryBuilder;
062import org.nuxeo.ecm.core.storage.mongodb.MongoDBConverter;
063import org.nuxeo.ecm.directory.BaseSession;
064import org.nuxeo.ecm.directory.DirectoryException;
065import org.nuxeo.ecm.directory.OperationNotAllowedException;
066import org.nuxeo.ecm.directory.PasswordHelper;
067import org.nuxeo.ecm.directory.Reference;
068import org.nuxeo.ecm.directory.Session;
069
070import com.mongodb.MongoWriteException;
071import com.mongodb.client.FindIterable;
072import com.mongodb.client.MongoCollection;
073import com.mongodb.client.MongoCursor;
074import com.mongodb.client.model.FindOneAndUpdateOptions;
075import com.mongodb.client.model.ReturnDocument;
076import com.mongodb.client.model.Updates;
077import com.mongodb.client.result.DeleteResult;
078import com.mongodb.client.result.UpdateResult;
079
080/**
081 * MongoDB implementation of a {@link Session}
082 *
083 * @since 9.1
084 */
085public class MongoDBSession extends BaseSession {
086
087    public MongoDBSession(MongoDBDirectory directory) {
088        super(directory, MongoDBReference.class);
089    }
090
091    @Override
092    public MongoDBDirectory getDirectory() {
093        return (MongoDBDirectory) directory;
094    }
095
096    @Override
097    public DocumentModel getEntryFromSource(String id, boolean fetchReferences) {
098        String idFieldName = getPrefixedIdField();
099        DocumentModelList result = doQuery(Collections.singletonMap(idFieldName, id), Collections.emptySet(),
100                Collections.emptyMap(), fetchReferences, 1, 0, false);
101
102        if (result.isEmpty()) {
103            return null;
104        }
105
106        DocumentModel docModel = result.get(0);
107
108        if (isMultiTenant()) {
109            // check that the entry is from the current tenant, or no tenant
110            // at all
111            if (!checkEntryTenantId((String) docModel.getProperty(schemaName, TENANT_ID_FIELD))) {
112                return null;
113            }
114        }
115        return docModel;
116    }
117
118    @Override
119    protected DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) {
120        // Make a copy of fieldMap to avoid modifying it
121        fieldMap = new HashMap<>(fieldMap);
122
123        // Filter out reference fields for creation as we keep it in a different collection
124        Map<String, Object> newDocMap = fieldMap.entrySet()
125                                                .stream()
126                                                .filter(entry -> getDirectory().getReferences(entry.getKey()) == null)
127                                                .collect(HashMap::new, (m, v) -> m.put(v.getKey(), v.getValue()),
128                                                        HashMap::putAll);
129        Map<String, Field> schemaFieldMap = directory.getSchemaFieldMap();
130        String idFieldName = getPrefixedIdField();
131        String id = String.valueOf(fieldMap.get(idFieldName));
132        if (autoincrementId) {
133            Document filter = MongoDBSerializationHelper.fieldMapToBson(MONGODB_ID, directoryName);
134            Bson update = Updates.inc(MONGODB_SEQ, 1L);
135            FindOneAndUpdateOptions options = new FindOneAndUpdateOptions().returnDocument(ReturnDocument.AFTER);
136            Long longId = getCountersCollection().findOneAndUpdate(filter, update, options).getLong(MONGODB_SEQ);
137            fieldMap.put(idFieldName, longId);
138            newDocMap.put(idFieldName, longId);
139            id = String.valueOf(longId);
140        }
141
142        if (isMultiTenant()) {
143            String tenantId = getCurrentTenantId();
144            if (StringUtils.isNotBlank(tenantId)) {
145                fieldMap.put(TENANT_ID_FIELD, tenantId);
146                newDocMap.put(TENANT_ID_FIELD, tenantId);
147                if (computeMultiTenantId) {
148                    id = computeMultiTenantDirectoryId(tenantId, id);
149                    fieldMap.put(idFieldName, id);
150                    newDocMap.put(idFieldName, id);
151                }
152            }
153        }
154
155        // Check if the entry already exists
156        if (hasEntry0(id)) {
157            throw new DirectoryException(
158                    String.format("Entry with id %s already exists in directory %s", id, directory.getName()),
159                    SC_CONFLICT);
160        }
161
162        try {
163
164            for (Map.Entry<String, Field> entry : schemaFieldMap.entrySet()) {
165                Field field = entry.getValue();
166                if (field != null) {
167                    String fieldName = field.getName().getPrefixedName();
168                    Type type = field.getType();
169                    newDocMap.computeIfPresent(fieldName, (k, v) -> convertToType(v, type));
170                    // Load default values if defined and not present in the map
171                    Object defaultValue = field.getDefaultValue();
172                    if (defaultValue != null) {
173                        newDocMap.putIfAbsent(fieldName, defaultValue);
174                    }
175                }
176            }
177            Document bson = MongoDBSerializationHelper.fieldMapToBson(newDocMap);
178            String password = (String) newDocMap.get(getPrefixedPasswordField());
179            if (password != null && !PasswordHelper.isHashed(password)) {
180                password = PasswordHelper.hashPassword(password, passwordHashAlgorithm);
181                bson.append(getPrefixedPasswordField(), password);
182            }
183            getCollection().insertOne(bson);
184        } catch (MongoWriteException e) {
185            throw new DirectoryException(e);
186        }
187        return createEntryModel(null, schemaName, String.valueOf(fieldMap.get(idFieldName)), fieldMap, isReadOnly());
188    }
189
190    protected Object convertToType(Object value, Type type) {
191        Object result = value;
192        if (value instanceof String) {
193            if (type instanceof IntegerType) {
194                result = Integer.valueOf((String) value);
195            } else if (type instanceof LongType) {
196                result = Long.valueOf((String) value);
197            }
198        } else if (value instanceof Number) {
199            if (type instanceof LongType && value instanceof Integer) {
200                result = Long.valueOf((Integer) value);
201            } else if (type instanceof StringType) {
202                result = value.toString();
203            }
204        }
205        return result;
206    }
207
208    @Override
209    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) {
210        Map<String, Object> fieldMap = new HashMap<>();
211        List<String> referenceFieldList = new LinkedList<>();
212
213        if (isMultiTenant()) {
214            // can only update entry from the current tenant
215            String tenantId = getCurrentTenantId();
216            if (StringUtils.isNotBlank(tenantId)) {
217                String entryTenantId = (String) docModel.getProperty(schemaName, TENANT_ID_FIELD);
218                if (isBlank(entryTenantId) || !entryTenantId.equals(tenantId)) {
219                    throw new OperationNotAllowedException("Operation not allowed in the current tenant context",
220                            "label.directory.error.multi.tenant.operationNotAllowed", null);
221                }
222            }
223        }
224
225        List<String> fields = directory.getSchemaFieldMap()
226                                       .values()
227                                       .stream()
228                                       .map(field -> field.getName().getPrefixedName())
229                                       .collect(Collectors.toList());
230        String idFieldName = getPrefixedIdField();
231        String passwordFieldName = getPrefixedPasswordField();
232
233        for (String fieldName : fields) {
234            if (fieldName.equals(idFieldName)) {
235                continue;
236            }
237            Property prop = docModel.getPropertyObject(schemaName, fieldName);
238            if (prop == null || !prop.isDirty()
239                    || (fieldName.equals(passwordFieldName) && StringUtils.isEmpty((String) prop.getValue()))) {
240                continue;
241            }
242            if (getDirectory().isReference(fieldName)) {
243                referenceFieldList.add(fieldName);
244            } else {
245                Serializable value = prop.getValue();
246                if (fieldName.equals(passwordFieldName)) {
247                    value = PasswordHelper.hashPassword((String) value, passwordHashAlgorithm);
248                }
249                if (value instanceof Calendar) {
250                    value = ((Calendar) value).getTime();
251                }
252                fieldMap.put(prop.getName(), value);
253            }
254        }
255
256        String id = docModel.getId();
257        Object idFieldValue = convertToType(id, getIdFieldType());
258        Document bson = MongoDBSerializationHelper.fieldMapToBson(idFieldName, idFieldValue);
259
260        List<Bson> updates = fieldMap.entrySet()
261                                     .stream()
262                                     .map(e -> Updates.set(e.getKey(), e.getValue()))
263                                     .collect(Collectors.toList());
264
265        if (!updates.isEmpty()) {
266            try {
267                UpdateResult result = getCollection().updateOne(bson, Updates.combine(updates));
268                // Throw an error if no document matched the update
269                if (!result.wasAcknowledged()) {
270                    throw new DirectoryException(
271                            "Error while updating the entry, the request has not been acknowledged by the server");
272                }
273                if (result.getMatchedCount() == 0) {
274                    throw new DirectoryException(
275                            String.format("Error while updating the entry, no document was found with the id %s", id));
276                }
277            } catch (MongoWriteException e) {
278                throw new DirectoryException(e);
279            }
280        }
281        return referenceFieldList;
282    }
283
284    @Override
285    public void deleteEntryWithoutReferences(String id) {
286        try {
287            String idFieldName = getPrefixedIdField();
288            Object idFieldValue = convertToType(id, getIdFieldType());
289            DeleteResult result = getCollection().deleteOne(
290                    MongoDBSerializationHelper.fieldMapToBson(idFieldName, idFieldValue));
291            if (!result.wasAcknowledged()) {
292                throw new DirectoryException(
293                        "Error while deleting the entry, the request has not been acknowledged by the server");
294            }
295        } catch (MongoWriteException e) {
296            throw new DirectoryException(e);
297        }
298    }
299
300    @Override
301    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
302            boolean fetchReferences, int limit, int offset) {
303        return doQuery(filter, fulltext, orderBy, fetchReferences, limit, offset, true);
304    }
305
306    protected DocumentModelList doQuery(Map<String, Serializable> filter, Set<String> fulltext,
307            Map<String, String> orderBy, boolean fetchReferences, int limit, int offset, boolean checkTenantId) {
308
309        if (!hasPermission(SecurityConstants.READ)) {
310            return new DocumentModelListImpl();
311        }
312
313        Map<String, Serializable> filterMap = new HashMap<>(filter);
314
315        if (checkTenantId && isMultiTenant()) {
316            // filter entries on the tenantId field also
317            String tenantId = getCurrentTenantId();
318            if (StringUtils.isNotBlank(tenantId)) {
319                filterMap.put(TENANT_ID_FIELD, tenantId);
320            }
321        }
322
323        // Remove password as it is not possible to do queries with it
324        String passwordFieldName = getPrefixedPasswordField();
325        filterMap.remove(passwordFieldName);
326        Document bson = buildQuery(filterMap, fulltext);
327
328        DocumentModelList entries = new DocumentModelListImpl();
329
330        FindIterable<Document> results = getCollection().find(bson).skip(offset);
331        if (limit > 0) {
332            results.limit(limit);
333        }
334        for (Document resultDoc : results) {
335
336            // Cast object to document model
337            Map<String, Object> fieldMap = MongoDBSerializationHelper.bsonToFieldMap(resultDoc);
338            // Remove password from results
339            if (!readAllColumns) {
340                fieldMap.remove(passwordFieldName);
341            }
342            DocumentModel doc = fieldMapToDocumentModel(fieldMap);
343
344            if (fetchReferences) {
345                Map<String, List<String>> targetIdsMap = new HashMap<>();
346                for (Reference reference : directory.getReferences()) {
347                    List<String> targetIds;
348                    if (reference instanceof MongoDBReference) {
349                        MongoDBReference mongoReference = (MongoDBReference) reference;
350                        targetIds = mongoReference.getTargetIdsForSource(doc.getId(), this);
351                    } else {
352                        targetIds = reference.getTargetIdsForSource(doc.getId());
353                    }
354                    targetIds = new ArrayList<>(targetIds);
355                    Collections.sort(targetIds);
356                    String fieldName = reference.getFieldName();
357                    targetIdsMap.computeIfAbsent(fieldName, key -> new ArrayList<>()).addAll(targetIds);
358                }
359                for (Map.Entry<String, List<String>> entry : targetIdsMap.entrySet()) {
360                    String fieldName = entry.getKey();
361                    List<String> targetIds = entry.getValue();
362                    try {
363                        doc.setProperty(schemaName, fieldName, targetIds);
364                    } catch (PropertyException e) {
365                        throw new DirectoryException(e);
366                    }
367                }
368            }
369            entries.add(doc);
370        }
371
372        if (orderBy != null && !orderBy.isEmpty()) {
373            getDirectory().orderEntries(entries, orderBy);
374        }
375
376        return entries;
377    }
378
379    protected Document buildQuery(Map<String, Serializable> fieldMap, Set<String> fulltext) {
380        Map<String, Field> schemaFieldMap = directory.getSchemaFieldMap();
381        Document bson = new Document();
382        for (Map.Entry<String, Serializable> entry : fieldMap.entrySet()) {
383            Field field = schemaFieldMap.values()
384                                        .stream()
385                                        .filter(v -> v.getName().getPrefixedName().equals(entry.getKey()))
386                                        .findFirst()
387                                        .orElse(null);
388
389            Serializable v = entry.getValue();
390            Object value = (field != null) ? MongoDBSerializationHelper.valueToBson(v, field.getType())
391                    : MongoDBSerializationHelper.valueToBson(v);
392            String key = entry.getKey();
393            if (fulltext != null && fulltext.contains(key)) {
394                String val = String.valueOf(value);
395                val = val.replaceAll("%+", ".*");
396                switch (substringMatchType) {
397                case subany:
398                    addField(bson, key, Pattern.compile(val, Pattern.CASE_INSENSITIVE));
399                    break;
400                case subinitial:
401                    addField(bson, key, Pattern.compile('^' + val, Pattern.CASE_INSENSITIVE));
402                    break;
403                case subfinal:
404                    addField(bson, key, Pattern.compile(val + '$', Pattern.CASE_INSENSITIVE));
405                    break;
406                }
407            } else {
408                addField(bson, key, value);
409            }
410        }
411        return bson;
412    }
413
414    protected void addField(Document bson, String key, Object value) {
415        String keyFieldName = key;
416        Field field = directory.getSchemaFieldMap().get(key);
417        if (field != null) {
418            keyFieldName = field.getName().getPrefixedName();
419        }
420        bson.put(keyFieldName, value);
421    }
422
423    @Override
424    public DocumentModelList query(QueryBuilder queryBuilder, boolean fetchReferences) {
425        if (!hasPermission(SecurityConstants.READ)) {
426            return new DocumentModelListImpl();
427        }
428        String passwordFieldName = getPrefixedPasswordField();
429        if (FieldDetector.hasField(queryBuilder.predicate(), getPasswordField())
430                || FieldDetector.hasField(queryBuilder.predicate(), passwordFieldName)) {
431            throw new DirectoryException("Cannot filter on password");
432        }
433        queryBuilder = addTenantId(queryBuilder);
434
435        MongoDBConverter converter = new MongoDBConverter();
436        MongoDBDirectoryQueryBuilder builder = new MongoDBDirectoryQueryBuilder(converter, queryBuilder.predicate());
437        builder.walk();
438        Document filter = builder.getQuery();
439        int limit = Math.max(0, (int) queryBuilder.limit());
440        int offset = Math.max(0, (int) queryBuilder.offset());
441        boolean countTotal = queryBuilder.countTotal();
442        // we should also use getDirectory().getDescriptor().getQuerySizeLimit() like in SQL
443        Document sort = builder.walkOrderBy(queryBuilder.orders());
444
445        DocumentModelListImpl entries = new DocumentModelListImpl();
446
447        // use a MongoCursor instead of a simple MongoIterable to avoid fetching everything at once
448        try (MongoCursor<Document> cursor = getCollection().find(filter)
449                                                           .limit(limit)
450                                                           .skip(offset)
451                                                           .sort(sort)
452                                                           .iterator()) {
453            for (Document doc : (Iterable<Document>) () -> cursor) {
454                if (!readAllColumns) {
455                    // remove password from results
456                    doc.remove(passwordFieldName);
457                }
458                State state = converter.bsonToState(doc);
459                Map<String, Object> fieldMap = new HashMap<>();
460                for (Entry<String, Serializable> es : state.entrySet()) {
461                    fieldMap.put(es.getKey(), es.getValue());
462                }
463                DocumentModel docModel = fieldMapToDocumentModel(fieldMap);
464
465                if (fetchReferences) {
466                    Map<String, List<String>> targetIdsMap = new HashMap<>();
467                    for (Reference reference : directory.getReferences()) {
468                        List<String> targetIds;
469                        if (reference instanceof MongoDBReference) {
470                            MongoDBReference mongoReference = (MongoDBReference) reference;
471                            targetIds = mongoReference.getTargetIdsForSource(docModel.getId(), this);
472                        } else {
473                            targetIds = reference.getTargetIdsForSource(docModel.getId());
474                        }
475                        targetIds = new ArrayList<>(targetIds);
476                        Collections.sort(targetIds);
477                        String fieldName = reference.getFieldName();
478                        targetIdsMap.computeIfAbsent(fieldName, key -> new ArrayList<>()).addAll(targetIds);
479                    }
480                    for (Entry<String, List<String>> entry : targetIdsMap.entrySet()) {
481                        String fieldName = entry.getKey();
482                        List<String> targetIds = entry.getValue();
483                        docModel.setProperty(schemaName, fieldName, targetIds);
484                    }
485                }
486                entries.add(docModel);
487            }
488        }
489        if (limit != 0 || offset != 0) {
490            long count;
491            if (countTotal) {
492                // we have to do an additional query to count the total number of results
493                count = getCollection().countDocuments(filter);
494            } else {
495                count = -2; // unknown
496            }
497            entries.setTotalSize(count);
498        }
499        return entries;
500    }
501
502    @Override
503    public List<String> queryIds(QueryBuilder queryBuilder) {
504        if (!hasPermission(SecurityConstants.READ)) {
505            return Collections.emptyList();
506        }
507        if (FieldDetector.hasField(queryBuilder.predicate(), getPasswordField())
508                || FieldDetector.hasField(queryBuilder.predicate(), getPrefixedPasswordField())) {
509            throw new DirectoryException("Cannot filter on password");
510        }
511        queryBuilder = addTenantId(queryBuilder);
512
513        MongoDBConverter converter = new MongoDBConverter();
514        MongoDBDirectoryQueryBuilder builder = new MongoDBDirectoryQueryBuilder(converter, queryBuilder.predicate());
515        builder.walk();
516        Document filter = builder.getQuery();
517        String idFieldName = getPrefixedIdField();
518        Document projection = new Document(idFieldName, 1L);
519        int limit = Math.max(0, (int) queryBuilder.limit());
520        int offset = Math.max(0, (int) queryBuilder.offset());
521        // we should also use getDirectory().getDescriptor().getQuerySizeLimit() like in SQL
522        Document sort = builder.walkOrderBy(queryBuilder.orders());
523
524        List<String> ids = new ArrayList<>();
525
526        // use a MongoCursor instead of a simple MongoIterable to avoid fetching everything at once
527        try (MongoCursor<Document> cursor = getCollection().find(filter)
528                                                           .projection(projection)
529                                                           .limit(limit)
530                                                           .skip(offset)
531                                                           .sort(sort)
532                                                           .iterator()) {
533            for (Document doc : (Iterable<Document>) () -> cursor) {
534                State state = converter.bsonToState(doc);
535                String id = getIdFromState(state);
536                ids.add(id);
537            }
538        }
539        return ids;
540    }
541
542    /**
543     * MongoDB Query Builder that knows how to resolved directory properties.
544     *
545     * @since 10.3
546     */
547    public class MongoDBDirectoryQueryBuilder extends MongoDBAbstractQueryBuilder {
548
549        public MongoDBDirectoryQueryBuilder(MongoDBConverter converter, Expression expression) {
550            super(converter, expression);
551        }
552
553        @Override
554        protected Document newDocumentWithField(FieldInfo fieldInfo, Object value) {
555            return new Document(fieldInfo.queryField, convertToType(value, fieldInfo.type));
556        }
557
558        @Override
559        protected FieldInfo walkReference(String name) {
560            Field field = directory.getSchemaFieldMap().get(name);
561            if (field == null) {
562                throw new QueryParseException("No column: " + name + " for directory: " + getDirectory().getName());
563            }
564            String key = field.getName().getPrefixedName();
565            String queryField = stripElemMatchPrefix(key);
566            return new FieldInfo(name, key, queryField, queryField, field.getType());
567        }
568
569        protected Document walkOrderBy(OrderByList orderByList) {
570            if (orderByList.isEmpty()) {
571                return null;
572            }
573            Document orderBy = new Document();
574            for (OrderByExpr ob : orderByList) {
575                String field = walkReference(ob.reference).queryField;
576                if (!orderBy.containsKey(field)) {
577                    orderBy.put(field, ob.isDescending ? MINUS_ONE : ONE);
578                }
579            }
580            return orderBy;
581        }
582    }
583
584    @Override
585    public void close() {
586        getDirectory().removeSession(this);
587    }
588
589    @Override
590    public boolean authenticate(String username, String password) {
591        Document user = getCollection().find(MongoDBSerializationHelper.fieldMapToBson(getPrefixedIdField(), username))
592                                       .first();
593        if (user == null) {
594            return false;
595        }
596
597        String storedPassword = user.getString(getPrefixedPasswordField());
598        if (isMultiTenant()) {
599            // check that the entry is from the current tenant, or no tenant at all
600            if (!checkEntryTenantId(user.getString(TENANT_ID_FIELD))) {
601                storedPassword = null;
602            }
603        }
604
605        return PasswordHelper.verifyPassword(password, storedPassword);
606    }
607
608    @Override
609    public boolean isAuthenticating() {
610        return directory.getSchemaFieldMap().containsKey(getPasswordField());
611    }
612
613    @Override
614    public boolean hasEntry(String id) {
615        return hasEntry0(id);
616    }
617
618    protected boolean hasEntry0(Object id) {
619        String idFieldName = getPrefixedIdField();
620        Type idFieldType = getIdFieldType();
621        Object idFieldValue = convertToType(id, idFieldType);
622        return getCollection().countDocuments(MongoDBSerializationHelper.fieldMapToBson(idFieldName, idFieldValue)) > 0;
623    }
624
625    /**
626     * Retrieve the collection associated to this directory
627     *
628     * @return the MongoDB collection
629     */
630    protected MongoCollection<Document> getCollection() {
631        return getDirectory().getCollection();
632    }
633
634    /**
635     * Retrieve the counters collection associated to this directory
636     *
637     * @return the MongoDB counters collection
638     */
639    protected MongoCollection<Document> getCountersCollection() {
640        return getDirectory().getCountersCollection();
641    }
642
643    protected DocumentModel fieldMapToDocumentModel(Map<String, Object> fieldMap) {
644        String idFieldName = getPrefixedIdField();
645        if (!fieldMap.containsKey(idFieldName)) {
646            idFieldName = getIdField();
647        }
648        String id = String.valueOf(fieldMap.get(idFieldName));
649        return createEntryModel(null, schemaName, id, fieldMap, isReadOnly());
650    }
651
652    protected String getIdFromState(State state) {
653        String idFieldName = getPrefixedIdField();
654        if (!state.containsKey(idFieldName)) {
655            idFieldName = getIdField();
656        }
657        return String.valueOf(state.get(idFieldName));
658    }
659
660    protected boolean checkEntryTenantId(String entryTenantId) {
661        // check that the entry is from the current tenant, or no tenant at all
662        String tenantId = getCurrentTenantId();
663        return isBlank(tenantId) || isBlank(entryTenantId) || tenantId.equals(entryTenantId);
664    }
665
666    protected String getPrefixedIdField() {
667        Field idField = directory.getSchemaFieldMap().get(getIdField());
668        if (idField == null) {
669            return null;
670        }
671        return idField.getName().getPrefixedName();
672    }
673
674    protected String getPrefixedPasswordField() {
675        Field passwordField = directory.getSchemaFieldMap().get(getPasswordField());
676        if (passwordField == null) {
677            return null;
678        }
679        return passwordField.getName().getPrefixedName();
680    }
681
682    protected Type getIdFieldType() {
683        Field idField = directory.getSchemaFieldMap().get(getIdField());
684        if (idField == null) {
685            return null;
686        }
687        return idField.getType();
688    }
689
690}