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