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