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