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}