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