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