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