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