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