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 (!isCurrentUserAllowed(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 (!isCurrentUserAllowed(SecurityConstants.READ)) {
410            return null;
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        if (!isCurrentUserAllowed(SecurityConstants.WRITE)) {
506            return null;
507        }
508        init();
509        final Object rawid = fieldMap.get(schemaIdField);
510        if (rawid == null) {
511            throw new DirectoryException(String.format("Entry is missing id field '%s'", schemaIdField));
512        }
513        final String id = String.valueOf(rawid); // XXX allow longs too
514        for (SourceInfo sourceInfo : sourceInfos) {
515            if (!sourceInfo.source.creation) {
516                continue;
517            }
518            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
519                Map<String, Object> map = new HashMap<String, Object>();
520                map.put(dirInfo.idField, id);
521                for (Entry<String, String> e : dirInfo.fromSource.entrySet()) {
522                    map.put(e.getValue(), fieldMap.get(e.getKey()));
523                }
524                dirInfo.getSession().createEntry(map);
525            }
526            return getEntry(id);
527        }
528        throw new DirectoryException(String.format("Directory '%s' has no source allowing creation",
529                getName()));
530    }
531
532    @Override
533    public void deleteEntry(DocumentModel docModel) {
534        deleteEntry(docModel.getId());
535    }
536
537    @Override
538    public void deleteEntry(String id) {
539        if (!isCurrentUserAllowed(SecurityConstants.WRITE)) {
540            return;
541        }
542        init();
543        for (SourceInfo sourceInfo : sourceInfos) {
544            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
545                // Check if the platform is able to manage entry
546                if (!sourceInfo.source.creation && !dirInfo.getSession().isReadOnly()) {
547                    // If not check if entry exist to prevent exception that may
548                    // stop the deletion loop to other subdirectories
549                    // Do not raise exception, because creation is not managed
550                    // by the platform
551                    DocumentModel docModel = dirInfo.getSession().getEntry(id);
552                    if (docModel == null) {
553                        log.warn(String.format(
554                                "MultiDirectory '%s' : The entry id '%s' could not be deleted on subdirectory '%s' because it does not exist",
555                                getName(), id, dirInfo.dirName));
556                    } else {
557                        dirInfo.getSession().deleteEntry(id);
558                    }
559                } else {
560                    dirInfo.getSession().deleteEntry(id);
561                }
562            }
563        }
564    }
565
566    @Override
567    public void deleteEntry(String id, Map<String, String> map) throws DirectoryException {
568        log.warn("Calling deleteEntry extended on multi directory");
569        deleteEntry(id);
570    }
571
572    private static void updateSubDirectoryEntry(SubDirectoryInfo dirInfo, Map<String, Object> fieldMap, String id,
573            boolean canCreateIfOptional) {
574        DocumentModel dirEntry = dirInfo.getSession().getEntry(id);
575        if (dirInfo.getSession().isReadOnly() || (dirEntry != null && isReadOnlyEntry(dirEntry))) {
576            return;
577        }
578        if (dirEntry == null && !canCreateIfOptional) {
579            // entry to update doesn't belong to this directory
580            return;
581        }
582        Map<String, Object> map = new HashMap<String, Object>();
583        map.put(dirInfo.idField, id);
584        for (Entry<String, String> e : dirInfo.fromSource.entrySet()) {
585            map.put(e.getValue(), fieldMap.get(e.getKey()));
586        }
587        if (map.size() > 1) {
588            if (canCreateIfOptional && dirInfo.isOptional && dirEntry == null) {
589                // if entry does not exist, create it
590                dirInfo.getSession().createEntry(map);
591            } else {
592                final DocumentModel entry = BaseSession.createEntryModel(null, dirInfo.dirSchemaName, id, null);
593                // Do not set dataModel values with constructor to force fields
594                // dirty
595                entry.getDataModel(dirInfo.dirSchemaName).setMap(map);
596                dirInfo.getSession().updateEntry(entry);
597            }
598        }
599    }
600
601    @Override
602    public void updateEntry(DocumentModel docModel) {
603        if (!isCurrentUserAllowed(SecurityConstants.WRITE)) {
604            return;
605        }
606        if (isReadOnly() || isReadOnlyEntry(docModel)) {
607            return;
608        }
609        init();
610        final String id = docModel.getId();
611        Map<String, Object> fieldMap = docModel.getDataModel(schemaName).getMap();
612        for (SourceInfo sourceInfo : sourceInfos) {
613            // check if entry exists in this source, in case it can be created
614            // in optional subdirectories
615            boolean canCreateIfOptional = false;
616            for (SubDirectoryInfo dirInfo : sourceInfo.requiredSubDirectoryInfos) {
617                if (!canCreateIfOptional) {
618                    canCreateIfOptional = dirInfo.getSession().getEntry(id) != null;
619                }
620                updateSubDirectoryEntry(dirInfo, fieldMap, id, false);
621            }
622            for (SubDirectoryInfo dirInfo : sourceInfo.optionalSubDirectoryInfos) {
623                updateSubDirectoryEntry(dirInfo, fieldMap, id, canCreateIfOptional);
624            }
625        }
626    }
627
628    @Override
629    public DocumentModelList query(Map<String, Serializable> filter) {
630        return query(filter, Collections.<String> emptySet());
631    }
632
633    @Override
634    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext) {
635        return query(filter, fulltext, Collections.<String, String> emptyMap());
636    }
637
638    @Override
639    @SuppressWarnings("boxing")
640    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy)
641            {
642        return query(filter, fulltext, orderBy, false);
643    }
644
645    @Override
646    @SuppressWarnings("boxing")
647    public DocumentModelList query(Map<String, Serializable> filter, Set<String> fulltext, Map<String, String> orderBy,
648            boolean fetchReferences) {
649        // list of entries
650        final DocumentModelList results = new DocumentModelListImpl();
651        if (!isCurrentUserAllowed(SecurityConstants.READ)) {
652            return results;
653        }
654        init();
655
656        // entry ids already seen (mapped to the source name)
657        final Map<String, String> seen = new HashMap<String, String>();
658        if (fulltext == null) {
659            fulltext = Collections.emptySet();
660        }
661        Set<String> readOnlyEntries = new HashSet<String>();
662
663        for (SourceInfo sourceInfo : sourceInfos) {
664            // accumulated map for each entry
665            final Map<String, Map<String, Object>> maps = new HashMap<String, Map<String, Object>>();
666            // number of dirs seen for each entry
667            final Map<String, Integer> counts = new HashMap<String, Integer>();
668
669            // list of optional dirs where filter matches default values
670            List<SubDirectoryInfo> optionalDirsMatching = new ArrayList<SubDirectoryInfo>();
671            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
672                // compute filter
673                final Map<String, Serializable> dirFilter = new HashMap<String, Serializable>();
674                for (Entry<String, Serializable> e : filter.entrySet()) {
675                    final String fieldName = dirInfo.fromSource.get(e.getKey());
676                    if (fieldName == null) {
677                        continue;
678                    }
679                    dirFilter.put(fieldName, e.getValue());
680                }
681                if (dirInfo.isOptional) {
682                    // check if filter matches directory default values
683                    boolean matches = true;
684                    for (Map.Entry<String, Serializable> dirFilterEntry : dirFilter.entrySet()) {
685                        Object defaultValue = dirInfo.defaultEntry.get(dirFilterEntry.getKey());
686                        Object filterValue = dirFilterEntry.getValue();
687                        if (defaultValue == null && filterValue != null) {
688                            matches = false;
689                        } else if (defaultValue != null && !defaultValue.equals(filterValue)) {
690                            matches = false;
691                        }
692                    }
693                    if (matches) {
694                        optionalDirsMatching.add(dirInfo);
695                    }
696                }
697                // compute fulltext
698                Set<String> dirFulltext = new HashSet<String>();
699                for (String sourceFieldName : fulltext) {
700                    final String fieldName = dirInfo.fromSource.get(sourceFieldName);
701                    if (fieldName != null) {
702                        dirFulltext.add(fieldName);
703                    }
704                }
705                // make query to subdirectory
706                DocumentModelList l = dirInfo.getSession().query(dirFilter, dirFulltext, null, fetchReferences);
707                for (DocumentModel entry : l) {
708                    final String id = entry.getId();
709                    Map<String, Object> map = maps.get(id);
710                    if (map == null) {
711                        map = new HashMap<String, Object>();
712                        maps.put(id, map);
713                        counts.put(id, 1);
714                    } else {
715                        counts.put(id, counts.get(id) + 1);
716                    }
717                    for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
718                        map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
719                    }
720                    if (BaseSession.isReadOnlyEntry(entry)) {
721                        readOnlyEntries.add(id);
722                    }
723                }
724            }
725            // add default entry values for optional dirs
726            for (SubDirectoryInfo dirInfo : optionalDirsMatching) {
727                // add entry for every data found in other dirs
728                Set<String> existingIds = new HashSet<String>(dirInfo.getSession().getProjection(
729                        Collections.<String, Serializable> emptyMap(), dirInfo.idField));
730                for (Entry<String, Map<String, Object>> result : maps.entrySet()) {
731                    final String id = result.getKey();
732                    if (!existingIds.contains(id)) {
733                        counts.put(id, counts.get(id) + 1);
734                        final Map<String, Object> map = result.getValue();
735                        for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
736                            String value = e.getValue();
737                            if (!map.containsKey(value)) {
738                                map.put(value, dirInfo.defaultEntry.get(e.getKey()));
739                            }
740                        }
741                    }
742                }
743            }
744            // intersection, ignore entries not in all subdirectories
745            final int numdirs = sourceInfo.subDirectoryInfos.size();
746            for (Iterator<String> it = maps.keySet().iterator(); it.hasNext();) {
747                final String id = it.next();
748                if (counts.get(id) != numdirs) {
749                    it.remove();
750                }
751            }
752            // now create entries
753            ((ArrayList<?>) results).ensureCapacity(results.size() + maps.size());
754            for (Entry<String, Map<String, Object>> e : maps.entrySet()) {
755                final String id = e.getKey();
756                if (seen.containsKey(id)) {
757                    log.warn(String.format("Entry '%s' is present in source '%s' but also in source '%s'. "
758                            + "The second one will be ignored.", id, seen.get(id), sourceInfo.source.name));
759                    continue;
760                }
761                final Map<String, Object> map = e.getValue();
762                seen.put(id, sourceInfo.source.name);
763                final DocumentModel entry = BaseSession.createEntryModel(null, schemaName, id, map,
764                        readOnlyEntries.contains(id));
765                results.add(entry);
766            }
767        }
768        if (orderBy != null && !orderBy.isEmpty()) {
769            getDirectory().orderEntries(results, orderBy);
770        }
771        return results;
772    }
773
774    @Override
775    public List<String> getProjection(Map<String, Serializable> filter, String columnName) {
776        return getProjection(filter, Collections.<String> emptySet(), columnName);
777    }
778
779    @Override
780    public List<String> getProjection(Map<String, Serializable> filter, Set<String> fulltext, String columnName)
781            {
782
783        // There's no way to do an efficient getProjection to a source with
784        // multiple subdirectories given the current API (we'd need an API that
785        // passes several columns).
786        // So just do a non-optimal implementation for now.
787
788        final DocumentModelList entries = query(filter, fulltext);
789        final List<String> results = new ArrayList<String>(entries.size());
790        for (DocumentModel entry : entries) {
791            final Object value = entry.getProperty(schemaName, columnName);
792            if (value == null) {
793                results.add(null);
794            } else {
795                results.add(value.toString());
796            }
797        }
798        return results;
799    }
800
801    @Override
802    public DocumentModel createEntry(DocumentModel entry) {
803        Map<String, Object> fieldMap = entry.getProperties(schemaName);
804        return createEntry(fieldMap);
805    }
806
807    @Override
808    public boolean hasEntry(String id) {
809        init();
810        for (SourceInfo sourceInfo : sourceInfos) {
811            for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
812                Session session = dirInfo.getSession();
813                if (session.hasEntry(id)) {
814                    return true;
815                }
816            }
817        }
818        return false;
819    }
820
821}