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