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