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