001/*
002 * (C) Copyright 2006-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 *     Florent Guillaume
018 *
019 * $Id: MultiDirectorySession.java 29556 2008-01-23 00:59:39Z jcarsique $
020 */
021
022package org.nuxeo.ecm.directory.multi;
023
024import java.io.Serializable;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033
034import org.apache.commons.lang3.StringUtils;
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.nuxeo.ecm.core.api.DocumentModel;
038import org.nuxeo.ecm.core.api.DocumentModelList;
039import org.nuxeo.ecm.core.api.PropertyException;
040import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
041import org.nuxeo.ecm.core.api.security.SecurityConstants;
042import org.nuxeo.ecm.core.schema.SchemaManager;
043import org.nuxeo.ecm.core.schema.types.Field;
044import org.nuxeo.ecm.core.schema.types.Schema;
045import org.nuxeo.ecm.directory.BaseSession;
046import org.nuxeo.ecm.directory.DirectoryException;
047import org.nuxeo.ecm.directory.Session;
048import org.nuxeo.ecm.directory.api.DirectoryService;
049import org.nuxeo.runtime.api.Framework;
050
051/**
052 * Directory session aggregating entries from different sources.
053 * <p>
054 * Each source can build an entry aggregating fields from one or several directories.
055 *
056 * @author Florent Guillaume
057 * @author Anahide Tchertchian
058 */
059public class MultiDirectorySession extends BaseSession {
060
061    private static final Log log = LogFactory.getLog(MultiDirectorySession.class);
062
063    private final DirectoryService directoryService;
064
065    private final String schemaIdField;
066
067    private List<SourceInfo> sourceInfos;
068
069    public MultiDirectorySession(MultiDirectory directory) {
070        super(directory, null);
071        directoryService = Framework.getService(DirectoryService.class);
072        MultiDirectoryDescriptor descriptor = directory.getDescriptor();
073        schemaIdField = descriptor.idField;
074    }
075
076    @Override
077    public MultiDirectory getDirectory() {
078        return (MultiDirectory) directory;
079    }
080
081    protected class SubDirectoryInfo {
082
083        final String dirName;
084
085        final String dirSchemaName;
086
087        final String idField;
088
089        final boolean isAuthenticating;
090
091        final Map<String, String> fromSource;
092
093        final Map<String, String> toSource;
094
095        final Map<String, Serializable> defaultEntry;
096
097        final boolean isOptional;
098
099        Session session;
100
101        SubDirectoryInfo(String dirName, String dirSchemaName, String idField, boolean isAuthenticating,
102                Map<String, String> fromSource, Map<String, String> toSource, Map<String, Serializable> defaultEntry,
103                boolean isOptional) {
104            this.dirName = dirName;
105            this.dirSchemaName = dirSchemaName;
106            this.idField = idField;
107            this.isAuthenticating = isAuthenticating;
108            this.fromSource = fromSource;
109            this.toSource = toSource;
110            this.defaultEntry = defaultEntry;
111            this.isOptional = isOptional;
112        }
113
114        Session getSession() throws DirectoryException {
115            if (session == null) {
116                session = directoryService.open(dirName);
117            }
118            return session;
119        }
120
121        @Override
122        public String toString() {
123            return String.format("{directory=%s fromSource=%s toSource=%s}", dirName, fromSource, toSource);
124        }
125    }
126
127    protected static class SourceInfo {
128
129        final SourceDescriptor source;
130
131        final List<SubDirectoryInfo> subDirectoryInfos;
132
133        final List<SubDirectoryInfo> requiredSubDirectoryInfos;
134
135        final List<SubDirectoryInfo> optionalSubDirectoryInfos;
136
137        final SubDirectoryInfo authDirectoryInfo;
138
139        SourceInfo(SourceDescriptor source, List<SubDirectoryInfo> subDirectoryInfos, SubDirectoryInfo authDirectoryInfo) {
140            this.source = source;
141            this.subDirectoryInfos = subDirectoryInfos;
142            requiredSubDirectoryInfos = new ArrayList<>();
143            optionalSubDirectoryInfos = new ArrayList<>();
144            for (SubDirectoryInfo subDirInfo : subDirectoryInfos) {
145                if (subDirInfo.isOptional) {
146                    optionalSubDirectoryInfos.add(subDirInfo);
147                } else {
148                    requiredSubDirectoryInfos.add(subDirInfo);
149                }
150            }
151            this.authDirectoryInfo = authDirectoryInfo;
152        }
153
154        @Override
155        public String toString() {
156            return String.format("{source=%s infos=%s}", source.name, subDirectoryInfos);
157        }
158    }
159
160    private void init() throws DirectoryException {
161        if (sourceInfos == null) {
162            recomputeSourceInfos();
163        }
164    }
165
166    /**
167     * Recomputes all the info needed for efficient access.
168     */
169    private void recomputeSourceInfos() throws DirectoryException {
170        SchemaManager schemaManager = Framework.getService(SchemaManager.class);
171        final Schema schema = schemaManager.getSchema(schemaName);
172        if (schema == null) {
173            throw new DirectoryException(String.format("Directory '%s' has unknown schema '%s'", getName(),
174                    schemaName));
175        }
176        final Set<String> sourceFields = new HashSet<>();
177        for (Field f : schema.getFields()) {
178            sourceFields.add(f.getName().getLocalName());
179        }
180        if (!sourceFields.contains(schemaIdField)) {
181            throw new DirectoryException(String.format("Directory '%s' schema '%s' has no id field '%s'",
182                    getName(), schemaName, schemaIdField));
183        }
184
185        List<SourceInfo> newSourceInfos = new ArrayList<>(2);
186        for (SourceDescriptor source : getDirectory().getDescriptor().sources) {
187            int ndirs = source.subDirectories.length;
188            if (ndirs == 0) {
189                throw new DirectoryException(String.format("Directory '%s' source '%s' has no subdirectories",
190                        getName(), source.name));
191            }
192
193            final List<SubDirectoryInfo> subDirectoryInfos = new ArrayList<>(ndirs);
194
195            SubDirectoryInfo authDirectoryInfo = null;
196            boolean hasRequiredDir = false;
197            for (SubDirectoryDescriptor subDir : source.subDirectories) {
198                final String dirName = subDir.name;
199                final String dirSchemaName = directoryService.getDirectorySchema(dirName);
200                final String dirIdField = directoryService.getDirectoryIdField(dirName);
201                final boolean dirIsAuth = directoryService.getDirectoryPasswordField(dirName) != null;
202                final Map<String, String> fromSource = new HashMap<>();
203                final Map<String, String> toSource = new HashMap<>();
204                final Map<String, Serializable> defaultEntry = new HashMap<>();
205                final boolean dirIsOptional = subDir.isOptional;
206
207                // XXX check authenticating
208                final Schema dirSchema = schemaManager.getSchema(dirSchemaName);
209                if (dirSchema == null) {
210                    throw new DirectoryException(String.format("Directory '%s' source '%s' subdirectory '%s' "
211                            + "has unknown schema '%s'", getName(), source.name, dirName, dirSchemaName));
212                }
213                // record default field mappings if same name and record default
214                // values
215                final Set<String> dirSchemaFields = new HashSet<>();
216                for (Field f : dirSchema.getFields()) {
217                    final String fieldName = f.getName().getLocalName();
218                    dirSchemaFields.add(fieldName);
219                    if (sourceFields.contains(fieldName)) {
220                        // XXX check no duplicates!
221                        fromSource.put(fieldName, fieldName);
222                        toSource.put(fieldName, fieldName);
223                    }
224                    // XXX cast to Serializable
225                    defaultEntry.put(fieldName, (Serializable) f.getDefaultValue());
226                }
227                // treat renamings
228                // XXX id field ?
229                for (FieldDescriptor field : subDir.fields) {
230                    final String sourceFieldName = field.forField;
231                    final String fieldName = field.name;
232                    if (!sourceFields.contains(sourceFieldName)) {
233                        throw new DirectoryException(String.format("Directory '%s' source '%s' subdirectory '%s' "
234                                + "has mapping for unknown field '%s'", getName(), source.name, dirName,
235                                sourceFieldName));
236                    }
237                    if (!dirSchemaFields.contains(fieldName)) {
238                        throw new DirectoryException(String.format("Directory '%s' source '%s' subdirectory '%s' "
239                                + "has mapping of unknown field' '%s'", getName(), source.name, dirName,
240                                fieldName));
241                    }
242                    fromSource.put(sourceFieldName, fieldName);
243                    toSource.put(fieldName, sourceFieldName);
244                }
245                SubDirectoryInfo subDirectoryInfo = new SubDirectoryInfo(dirName, dirSchemaName, dirIdField, dirIsAuth,
246                        fromSource, toSource, defaultEntry, dirIsOptional);
247                subDirectoryInfos.add(subDirectoryInfo);
248
249                if (dirIsAuth) {
250                    if (authDirectoryInfo != null) {
251                        throw new DirectoryException(String.format("Directory '%s' source '%s' has two subdirectories "
252                                + "with a password field, '%s' and '%s'", getName(), source.name,
253                                authDirectoryInfo.dirName, dirName));
254                    }
255                    authDirectoryInfo = subDirectoryInfo;
256                }
257                if (!dirIsOptional) {
258                    hasRequiredDir = true;
259                }
260            }
261            if (isAuthenticating() && authDirectoryInfo == null) {
262                throw new DirectoryException(String.format("Directory '%s' source '%s' has no subdirectory "
263                        + "with a password field", getName(), source.name));
264            }
265            if (!hasRequiredDir) {
266                throw new DirectoryException(String.format(
267                        "Directory '%s' source '%s' only has optional subdirectories: "
268                                + "no directory can be used has a reference.", getName(), source.name));
269            }
270            newSourceInfos.add(new SourceInfo(source, subDirectoryInfos, authDirectoryInfo));
271        }
272        sourceInfos = newSourceInfos;
273    }
274
275    @Override
276    public void close() throws DirectoryException {
277        try {
278            if (sourceInfos == null) {
279                return;
280            }
281            DirectoryException exc = null;
282            for (SourceInfo sourceInfo : sourceInfos) {
283                for (SubDirectoryInfo subDirectoryInfo : sourceInfo.subDirectoryInfos) {
284                    Session session = subDirectoryInfo.session;
285                    subDirectoryInfo.session = null;
286                    if (session != null) {
287                        try {
288                            session.close();
289                        } catch (DirectoryException e) {
290                            // remember exception, we want to close all session
291                            // first
292                            if (exc == null) {
293                                exc = e;
294                            } else {
295                                // we can't reraise both, log this one
296                                log.error("Error closing directory " + subDirectoryInfo.dirName, e);
297                            }
298                        }
299                    }
300                }
301                if (exc != null) {
302                    throw exc;
303                }
304            }
305        } finally {
306            getDirectory().removeSession(this);
307        }
308    }
309
310    public String getName() {
311        return directory.getName();
312    }
313
314    @Override
315    public boolean authenticate(String username, String password) {
316        init();
317        for (SourceInfo sourceInfo : sourceInfos) {
318            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
319                if (!dirInfo.isAuthenticating) {
320                    continue;
321                }
322                if (dirInfo.getSession().authenticate(username, password)) {
323                    return true;
324                }
325                if (dirInfo.isOptional && dirInfo.getSession().getEntry(username) == null) {
326                    // check if given password equals to default value
327                    String passwordField = dirInfo.getSession().getPasswordField();
328                    String defaultPassword = (String) dirInfo.defaultEntry.get(passwordField);
329                    if (defaultPassword != null && defaultPassword.equals(password)) {
330                        return true;
331                    }
332                }
333            }
334        }
335        return false;
336    }
337
338    @Override
339    public DocumentModel getEntry(String id, boolean fetchReferences) throws DirectoryException {
340        if (!hasPermission(SecurityConstants.READ)) {
341            return null;
342        }
343        init();
344        String entryId = id;
345        source_loop: for (SourceInfo sourceInfo : sourceInfos) {
346            boolean isReadOnlyEntry = true;
347            final Map<String, Object> map = new HashMap<>();
348
349            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
350                final DocumentModel entry = dirInfo.getSession().getEntry(id, fetchReferences);
351                boolean isOptional = dirInfo.isOptional;
352                if (entry == null && !isOptional) {
353                    // not in this source
354                    continue source_loop;
355                }
356                if (entry != null && !isReadOnlyEntry(entry)) {
357                    // set readonly to false if at least one source is writable
358                    isReadOnlyEntry = false;
359                }
360                if (entry == null && isOptional && !dirInfo.getSession().isReadOnly()) {
361                    // set readonly to false if null entry is from optional and writable directory
362                    isReadOnlyEntry = false;
363                }
364                if (entry != null && StringUtils.isNotBlank(entry.getId())) {
365                    entryId = entry.getId();
366                }
367                for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
368                    if (entry != null) {
369                        try {
370                            map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
371                        } catch (PropertyException e1) {
372                            throw new DirectoryException(e1);
373                        }
374                    } else {
375                        // fill with default values for this directory
376                        if (!map.containsKey(e.getValue())) {
377                            map.put(e.getValue(), dirInfo.defaultEntry.get(e.getKey()));
378                        }
379                    }
380                }
381            }
382            // force the entry in readonly if it's defined on the multidirectory
383            if (isReadOnly()) {
384                isReadOnlyEntry = true;
385            }
386            // ok we have the data
387            try {
388                return BaseSession.createEntryModel(null, schemaName, entryId, map, isReadOnlyEntry);
389            } catch (PropertyException e) {
390                throw new DirectoryException(e);
391            }
392        }
393        return null;
394    }
395
396    @Override
397    @SuppressWarnings("boxing")
398    public DocumentModelList getEntries() {
399        if (!hasPermission(SecurityConstants.READ)) {
400            return new DocumentModelListImpl();
401        }
402        init();
403
404        // list of entries
405        final DocumentModelList results = new DocumentModelListImpl();
406        // entry ids already seen (mapped to the source name)
407        final Map<String, String> seen = new HashMap<>();
408        Set<String> readOnlyEntries = new HashSet<>();
409
410        for (SourceInfo sourceInfo : sourceInfos) {
411            // accumulated map for each entry
412            final Map<String, Map<String, Object>> maps = new HashMap<>();
413            // number of dirs seen for each entry
414            final Map<String, Integer> counts = new HashMap<>();
415            for (SubDirectoryInfo dirInfo : sourceInfo.requiredSubDirectoryInfos) {
416                final DocumentModelList entries = dirInfo.getSession().getEntries();
417                for (DocumentModel entry : entries) {
418                    final String id = entry.getId();
419                    // find or create map for this entry
420                    Map<String, Object> map = maps.get(id);
421                    if (map == null) {
422                        map = new HashMap<>();
423                        maps.put(id, map);
424                        counts.put(id, 1);
425                    } else {
426                        counts.put(id, counts.get(id) + 1);
427                    }
428                    // put entry data in map
429                    for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
430                        map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
431                    }
432                    if (BaseSession.isReadOnlyEntry(entry)) {
433                        readOnlyEntries.add(id);
434                    }
435                }
436            }
437            for (SubDirectoryInfo dirInfo : sourceInfo.optionalSubDirectoryInfos) {
438                final DocumentModelList entries = dirInfo.getSession().getEntries();
439                Set<String> existingIds = new HashSet<>();
440                for (DocumentModel entry : entries) {
441                    final String id = entry.getId();
442                    final Map<String, Object> map = maps.get(id);
443                    if (map != null) {
444                        existingIds.add(id);
445                        // put entry data in map
446                        for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
447                            map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
448                        }
449                    } else {
450                        log.warn(String.format("Entry '%s' for source '%s' is present in optional directory '%s' "
451                                + "but not in any required one. " + "It will be skipped.", id, sourceInfo.source.name,
452                                dirInfo.dirName));
453                    }
454                }
455                for (Entry<String, Map<String, Object>> mapEntry : maps.entrySet()) {
456                    if (!existingIds.contains(mapEntry.getKey())) {
457                        final Map<String, Object> map = mapEntry.getValue();
458                        // put entry data in map
459                        for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
460                            // fill with default values for this directory
461                            if (!map.containsKey(e.getValue())) {
462                                map.put(e.getValue(), dirInfo.defaultEntry.get(e.getKey()));
463                            }
464                        }
465                    }
466                }
467            }
468            // now create entries for all full maps
469            int numdirs = sourceInfo.requiredSubDirectoryInfos.size();
470            ((ArrayList<?>) results).ensureCapacity(results.size() + maps.size());
471            for (Entry<String, Map<String, Object>> e : maps.entrySet()) {
472                final String id = e.getKey();
473                if (seen.containsKey(id)) {
474                    log.warn(String.format("Entry '%s' is present in source '%s' but also in source '%s'. "
475                            + "The second one will be ignored.", id, seen.get(id), sourceInfo.source.name));
476                    continue;
477                }
478                final Map<String, Object> map = e.getValue();
479                if (counts.get(id) != numdirs) {
480                    log.warn(String.format("Entry '%s' for source '%s' is not present in all directories. "
481                            + "It will be skipped.", id, sourceInfo.source.name));
482                    continue;
483                }
484                seen.put(id, sourceInfo.source.name);
485                final DocumentModel entry = BaseSession.createEntryModel(null, schemaName, id, map,
486                        readOnlyEntries.contains(id));
487                results.add(entry);
488            }
489        }
490        return results;
491    }
492
493    @Override
494    public DocumentModel createEntryWithoutReferences(Map<String, Object> fieldMap) throws DirectoryException {
495        init();
496        final Object rawid = fieldMap.get(schemaIdField);
497        if (rawid == null) {
498            throw new DirectoryException(String.format("Entry is missing id field '%s'", schemaIdField));
499        }
500        final String id = String.valueOf(rawid); // XXX allow longs too
501        for (SourceInfo sourceInfo : sourceInfos) {
502            if (!sourceInfo.source.creation) {
503                continue;
504            }
505            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
506                Map<String, Object> map = new HashMap<>();
507                map.put(dirInfo.idField, id);
508                for (Entry<String, String> e : dirInfo.fromSource.entrySet()) {
509                    map.put(e.getValue(), fieldMap.get(e.getKey()));
510                }
511                dirInfo.getSession().createEntry(map);
512            }
513            return getEntry(id);
514        }
515        throw new DirectoryException(String.format("Directory '%s' has no source allowing creation", getName()));
516    }
517
518    @Override
519    protected List<String> updateEntryWithoutReferences(DocumentModel docModel) throws DirectoryException {
520        throw new UnsupportedOperationException();
521    }
522
523    @Override protected void deleteEntryWithoutReferences(String id) throws DirectoryException {
524        throw new UnsupportedOperationException();
525    }
526
527    @Override
528    public void deleteEntry(DocumentModel docModel) {
529        deleteEntry(docModel.getId());
530    }
531
532    @Override
533    public void deleteEntry(String id) {
534        checkPermission(SecurityConstants.WRITE);
535        checkDeleteConstraints(id);
536        init();
537        for (SourceInfo sourceInfo : sourceInfos) {
538            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
539                if (!dirInfo.getSession().isReadOnly()) {
540                    // Check if the platform is able to manage entry
541                    if (!sourceInfo.source.creation) {
542                        // If not check if entry exist to prevent exception that may
543                        // stop the deletion loop to other subdirectories
544                        // Do not raise exception, because creation is not managed
545                        // by the platform
546                        DocumentModel docModel = dirInfo.getSession().getEntry(id);
547                        if (docModel == null) {
548                            log.warn(String.format(
549                                    "MultiDirectory '%s' : The entry id '%s' could not be deleted on subdirectory '%s' because it does not exist",
550                                    getName(), id, dirInfo.dirName));
551                        } else {
552                            dirInfo.getSession().deleteEntry(id);
553                        }
554                    } else {
555                        dirInfo.getSession().deleteEntry(id);
556                    }
557                }
558            }
559        }
560    }
561
562    @Override
563    public void deleteEntry(String id, Map<String, String> map) throws DirectoryException {
564        log.warn("Calling deleteEntry extended on multi directory");
565        deleteEntry(id);
566    }
567
568    private static void updateSubDirectoryEntry(SubDirectoryInfo dirInfo, Map<String, Object> fieldMap, String id,
569            boolean canCreateIfOptional) {
570        DocumentModel dirEntry = dirInfo.getSession().getEntry(id);
571        if (dirInfo.getSession().isReadOnly() || (dirEntry != null && isReadOnlyEntry(dirEntry))) {
572            return;
573        }
574        if (dirEntry == null && !canCreateIfOptional) {
575            // entry to update doesn't belong to this directory
576            return;
577        }
578        Map<String, Object> map = new HashMap<>();
579        map.put(dirInfo.idField, id);
580        for (Entry<String, String> e : dirInfo.fromSource.entrySet()) {
581            map.put(e.getValue(), fieldMap.get(e.getKey()));
582        }
583        if (map.size() > 1) {
584            if (canCreateIfOptional && dirInfo.isOptional && dirEntry == null) {
585                // if entry does not exist, create it
586                dirInfo.getSession().createEntry(map);
587            } else {
588                final DocumentModel entry = BaseSession.createEntryModel(null, dirInfo.dirSchemaName, id, null);
589                entry.setProperties(dirInfo.dirSchemaName, map);
590                dirInfo.getSession().updateEntry(entry);
591            }
592        }
593    }
594
595    @Override
596    public void updateEntry(DocumentModel docModel) {
597        checkPermission(SecurityConstants.WRITE);
598        if (isReadOnlyEntry(docModel)) {
599            return;
600        }
601        init();
602        final String id = docModel.getId();
603        Map<String, Object> fieldMap = docModel.getProperties(schemaName);
604        for (SourceInfo sourceInfo : sourceInfos) {
605            // check if entry exists in this source, in case it can be created
606            // in optional subdirectories
607            boolean canCreateIfOptional = false;
608            for (SubDirectoryInfo dirInfo : sourceInfo.requiredSubDirectoryInfos) {
609                if (!canCreateIfOptional) {
610                    canCreateIfOptional = dirInfo.getSession().getEntry(id) != null;
611                }
612                updateSubDirectoryEntry(dirInfo, fieldMap, id, false);
613            }
614            for (SubDirectoryInfo dirInfo : sourceInfo.optionalSubDirectoryInfos) {
615                updateSubDirectoryEntry(dirInfo, fieldMap, id, canCreateIfOptional);
616            }
617        }
618    }
619
620    @Override
621    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
622            boolean fetchReferences, int limit, int offset) {
623        if (!hasPermission(SecurityConstants.READ)) {
624            return new DocumentModelListImpl();
625        }
626        init();
627
628        // entry ids already seen (mapped to the source name)
629        final Map<String, String> seen = new HashMap<>();
630        if (fulltext == null) {
631            fulltext = Collections.emptySet();
632        }
633        Set<String> readOnlyEntries = new HashSet<>();
634
635        DocumentModelList results = new DocumentModelListImpl();
636        for (SourceInfo sourceInfo : sourceInfos) {
637            // accumulated map for each entry
638            final Map<String, Map<String, Object>> maps = new HashMap<>();
639            // number of dirs seen for each entry
640            final Map<String, Integer> counts;
641            counts = new HashMap<>();
642
643            // list of optional dirs where filter matches default values
644            List<SubDirectoryInfo> optionalDirsMatching = new ArrayList<>();
645            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
646                // compute filter
647                final Map<String, Serializable> dirFilter = new HashMap<>();
648                for (Entry<String, Serializable> e : filter.entrySet()) {
649                    final String fieldName = dirInfo.fromSource.get(e.getKey());
650                    if (fieldName == null) {
651                        continue;
652                    }
653                    dirFilter.put(fieldName, e.getValue());
654                }
655                if (dirInfo.isOptional) {
656                    // check if filter matches directory default values
657                    boolean matches = true;
658                    for (Map.Entry<String, Serializable> dirFilterEntry : dirFilter.entrySet()) {
659                        Object defaultValue = dirInfo.defaultEntry.get(dirFilterEntry.getKey());
660                        Object filterValue = dirFilterEntry.getValue();
661                        if (defaultValue == null && filterValue != null) {
662                            matches = false;
663                        } else if (defaultValue != null && !defaultValue.equals(filterValue)) {
664                            matches = false;
665                        }
666                    }
667                    if (matches) {
668                        optionalDirsMatching.add(dirInfo);
669                    }
670                }
671                // compute fulltext
672                Set<String> dirFulltext = new HashSet<>();
673                for (String sourceFieldName : fulltext) {
674                    final String fieldName = dirInfo.fromSource.get(sourceFieldName);
675                    if (fieldName != null) {
676                        dirFulltext.add(fieldName);
677                    }
678                }
679                // make query to subdirectory
680                DocumentModelList l = dirInfo.getSession().query(dirFilter, dirFulltext, null, fetchReferences);
681                for (DocumentModel entry : l) {
682                    final String id = entry.getId();
683                    Map<String, Object> map = maps.get(id);
684                    if (map == null) {
685                        map = new HashMap<>();
686                        maps.put(id, map);
687                        counts.put(id, 1);
688                    } else {
689                        counts.put(id, counts.get(id) + 1);
690                    }
691                    for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
692                        map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
693                    }
694                    if (BaseSession.isReadOnlyEntry(entry)) {
695                        readOnlyEntries.add(id);
696                    }
697                }
698            }
699            // add default entry values for optional dirs
700            for (SubDirectoryInfo dirInfo : optionalDirsMatching) {
701                // add entry for every data found in other dirs
702                Set<String> existingIds = new HashSet<>(
703                        dirInfo.getSession().getProjection(Collections.emptyMap(), dirInfo.idField));
704                for (Entry<String, Map<String, Object>> result : maps.entrySet()) {
705                    final String id = result.getKey();
706                    if (!existingIds.contains(id)) {
707                        counts.put(id, counts.get(id) + 1);
708                        final Map<String, Object> map = result.getValue();
709                        for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
710                            String value = e.getValue();
711                            if (!map.containsKey(value)) {
712                                map.put(value, dirInfo.defaultEntry.get(e.getKey()));
713                            }
714                        }
715                    }
716                }
717            }
718            // intersection, ignore entries not in all subdirectories
719            final int numdirs = sourceInfo.subDirectoryInfos.size();
720            maps.keySet().removeIf(id -> counts.get(id) != numdirs);
721            // now create entries
722            ((ArrayList<?>) results).ensureCapacity(results.size() + maps.size());
723            for (Entry<String, Map<String, Object>> e : maps.entrySet()) {
724                final String id = e.getKey();
725                if (seen.containsKey(id)) {
726                    log.warn(String.format("Entry '%s' is present in source '%s' but also in source '%s'. "
727                            + "The second one will be ignored.", id, seen.get(id), sourceInfo.source.name));
728                    continue;
729                }
730                final Map<String, Object> map = e.getValue();
731                seen.put(id, sourceInfo.source.name);
732                final DocumentModel entry = BaseSession.createEntryModel(null, schemaName, id, map,
733                        readOnlyEntries.contains(id));
734                results.add(entry);
735            }
736        }
737        if (orderBy != null && !orderBy.isEmpty()) {
738            getDirectory().orderEntries(results, orderBy);
739        }
740        return applyQueryLimits(results, limit, offset);
741    }
742
743    @Override
744    public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName)
745            {
746
747        // There's no way to do an efficient getProjection to a source with
748        // multiple subdirectories given the current API (we'd need an API that
749        // passes several columns).
750        // So just do a non-optimal implementation for now.
751
752        final DocumentModelList entries = query(filter, fulltext);
753        final List<String> results = new ArrayList<>(entries.size());
754        for (DocumentModel entry : entries) {
755            final Object value = entry.getProperty(schemaName, columnName);
756            if (value == null) {
757                results.add(null);
758            } else {
759                results.add(value.toString());
760            }
761        }
762        return results;
763    }
764
765    @Override
766    public DocumentModel createEntry(DocumentModel entry) {
767        Map<String, Object> fieldMap = entry.getProperties(schemaName);
768        return createEntry(fieldMap);
769    }
770
771    @Override
772    public boolean hasEntry(String id) {
773        init();
774        for (SourceInfo sourceInfo : sourceInfos) {
775            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
776                Session session = dirInfo.getSession();
777                if (session.hasEntry(id)) {
778                    return true;
779                }
780            }
781        }
782        return false;
783    }
784
785}