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