@Test
  public void testReadOnlyEntryFromMultidirectory() throws Exception {
    MultiDirectory readonlyMultidir =
        (MultiDirectory) directoryService.getDirectory("readonlymulti");
    MultiDirectorySession readonlyDir = (MultiDirectorySession) readonlyMultidir.getSession();

    // all should be readonly
    assertTrue(BaseSession.isReadOnlyEntry(readonlyDir.getEntry("1")));
    assertTrue(BaseSession.isReadOnlyEntry(readonlyDir.getEntry("2")));
    assertTrue(BaseSession.isReadOnlyEntry(readonlyDir.getEntry("3")));
    assertTrue(BaseSession.isReadOnlyEntry(readonlyDir.getEntry("4")));
  }
  @Override
  @SuppressWarnings("boxing")
  public DocumentModelList query(
      Map<String, Serializable> filter,
      Set<String> fulltext,
      Map<String, String> orderBy,
      boolean fetchReferences) {
    // list of entries
    final DocumentModelList results = new DocumentModelListImpl();
    if (!isCurrentUserAllowed(SecurityConstants.READ)) {
      return results;
    }
    init();

    // entry ids already seen (mapped to the source name)
    final Map<String, String> seen = new HashMap<String, String>();
    if (fulltext == null) {
      fulltext = Collections.emptySet();
    }
    Set<String> readOnlyEntries = new HashSet<String>();

    for (SourceInfo sourceInfo : sourceInfos) {
      // accumulated map for each entry
      final Map<String, Map<String, Object>> maps = new HashMap<String, Map<String, Object>>();
      // number of dirs seen for each entry
      final Map<String, Integer> counts = new HashMap<String, Integer>();

      // list of optional dirs where filter matches default values
      List<SubDirectoryInfo> optionalDirsMatching = new ArrayList<SubDirectoryInfo>();
      for (SubDirectoryInfo dirInfo : sourceInfo.subDirectoryInfos) {
        // compute filter
        final Map<String, Serializable> dirFilter = new HashMap<String, Serializable>();
        for (Entry<String, Serializable> e : filter.entrySet()) {
          final String fieldName = dirInfo.fromSource.get(e.getKey());
          if (fieldName == null) {
            continue;
          }
          dirFilter.put(fieldName, e.getValue());
        }
        if (dirInfo.isOptional) {
          // check if filter matches directory default values
          boolean matches = true;
          for (Map.Entry<String, Serializable> dirFilterEntry : dirFilter.entrySet()) {
            Object defaultValue = dirInfo.defaultEntry.get(dirFilterEntry.getKey());
            Object filterValue = dirFilterEntry.getValue();
            if (defaultValue == null && filterValue != null) {
              matches = false;
            } else if (defaultValue != null && !defaultValue.equals(filterValue)) {
              matches = false;
            }
          }
          if (matches) {
            optionalDirsMatching.add(dirInfo);
          }
        }
        // compute fulltext
        Set<String> dirFulltext = new HashSet<String>();
        for (String sourceFieldName : fulltext) {
          final String fieldName = dirInfo.fromSource.get(sourceFieldName);
          if (fieldName != null) {
            dirFulltext.add(fieldName);
          }
        }
        // make query to subdirectory
        DocumentModelList l =
            dirInfo.getSession().query(dirFilter, dirFulltext, null, fetchReferences);
        for (DocumentModel entry : l) {
          final String id = entry.getId();
          Map<String, Object> map = maps.get(id);
          if (map == null) {
            map = new HashMap<String, Object>();
            maps.put(id, map);
            counts.put(id, 1);
          } else {
            counts.put(id, counts.get(id) + 1);
          }
          for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
            map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
          }
          if (BaseSession.isReadOnlyEntry(entry)) {
            readOnlyEntries.add(id);
          }
        }
      }
      // add default entry values for optional dirs
      for (SubDirectoryInfo dirInfo : optionalDirsMatching) {
        // add entry for every data found in other dirs
        Set<String> existingIds =
            new HashSet<String>(
                dirInfo
                    .getSession()
                    .getProjection(Collections.<String, Serializable>emptyMap(), dirInfo.idField));
        for (Entry<String, Map<String, Object>> result : maps.entrySet()) {
          final String id = result.getKey();
          if (!existingIds.contains(id)) {
            counts.put(id, counts.get(id) + 1);
            final Map<String, Object> map = result.getValue();
            for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
              String value = e.getValue();
              if (!map.containsKey(value)) {
                map.put(value, dirInfo.defaultEntry.get(e.getKey()));
              }
            }
          }
        }
      }
      // intersection, ignore entries not in all subdirectories
      final int numdirs = sourceInfo.subDirectoryInfos.size();
      for (Iterator<String> it = maps.keySet().iterator(); it.hasNext(); ) {
        final String id = it.next();
        if (counts.get(id) != numdirs) {
          it.remove();
        }
      }
      // now create entries
      ((ArrayList<?>) results).ensureCapacity(results.size() + maps.size());
      for (Entry<String, Map<String, Object>> e : maps.entrySet()) {
        final String id = e.getKey();
        if (seen.containsKey(id)) {
          log.warn(
              String.format(
                  "Entry '%s' is present in source '%s' but also in source '%s'. "
                      + "The second one will be ignored.",
                  id, seen.get(id), sourceInfo.source.name));
          continue;
        }
        final Map<String, Object> map = e.getValue();
        seen.put(id, sourceInfo.source.name);
        final DocumentModel entry =
            BaseSession.createEntryModel(null, schemaName, id, map, readOnlyEntries.contains(id));
        results.add(entry);
      }
    }
    if (orderBy != null && !orderBy.isEmpty()) {
      directory.orderEntries(results, orderBy);
    }
    return results;
  }
  @Override
  @SuppressWarnings("boxing")
  public DocumentModelList getEntries() {
    if (!isCurrentUserAllowed(SecurityConstants.READ)) {
      return null;
    }
    init();

    // list of entries
    final DocumentModelList results = new DocumentModelListImpl();
    // entry ids already seen (mapped to the source name)
    final Map<String, String> seen = new HashMap<String, String>();
    Set<String> readOnlyEntries = new HashSet<String>();

    for (SourceInfo sourceInfo : sourceInfos) {
      // accumulated map for each entry
      final Map<String, Map<String, Object>> maps = new HashMap<String, Map<String, Object>>();
      // number of dirs seen for each entry
      final Map<String, Integer> counts = new HashMap<String, Integer>();
      for (SubDirectoryInfo dirInfo : sourceInfo.requiredSubDirectoryInfos) {
        final DocumentModelList entries = dirInfo.getSession().getEntries();
        for (DocumentModel entry : entries) {
          final String id = entry.getId();
          // find or create map for this entry
          Map<String, Object> map = maps.get(id);
          if (map == null) {
            map = new HashMap<String, Object>();
            maps.put(id, map);
            counts.put(id, 1);
          } else {
            counts.put(id, counts.get(id) + 1);
          }
          // put entry data in map
          for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
            map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
          }
          if (BaseSession.isReadOnlyEntry(entry)) {
            readOnlyEntries.add(id);
          }
        }
      }
      for (SubDirectoryInfo dirInfo : sourceInfo.optionalSubDirectoryInfos) {
        final DocumentModelList entries = dirInfo.getSession().getEntries();
        Set<String> existingIds = new HashSet<String>();
        for (DocumentModel entry : entries) {
          final String id = entry.getId();
          final Map<String, Object> map = maps.get(id);
          if (map != null) {
            existingIds.add(id);
            // put entry data in map
            for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
              map.put(e.getValue(), entry.getProperty(dirInfo.dirSchemaName, e.getKey()));
            }
          } else {
            log.warn(
                String.format(
                    "Entry '%s' for source '%s' is present in optional directory '%s' "
                        + "but not in any required one. "
                        + "It will be skipped.",
                    id, sourceInfo.source.name, dirInfo.dirName));
          }
        }
        for (Entry<String, Map<String, Object>> mapEntry : maps.entrySet()) {
          if (!existingIds.contains(mapEntry.getKey())) {
            final Map<String, Object> map = mapEntry.getValue();
            // put entry data in map
            for (Entry<String, String> e : dirInfo.toSource.entrySet()) {
              // fill with default values for this directory
              if (!map.containsKey(e.getValue())) {
                map.put(e.getValue(), dirInfo.defaultEntry.get(e.getKey()));
              }
            }
          }
        }
      }
      // now create entries for all full maps
      int numdirs = sourceInfo.requiredSubDirectoryInfos.size();
      ((ArrayList<?>) results).ensureCapacity(results.size() + maps.size());
      for (Entry<String, Map<String, Object>> e : maps.entrySet()) {
        final String id = e.getKey();
        if (seen.containsKey(id)) {
          log.warn(
              String.format(
                  "Entry '%s' is present in source '%s' but also in source '%s'. "
                      + "The second one will be ignored.",
                  id, seen.get(id), sourceInfo.source.name));
          continue;
        }
        final Map<String, Object> map = e.getValue();
        if (counts.get(id) != numdirs) {
          log.warn(
              String.format(
                  "Entry '%s' for source '%s' is not present in all directories. "
                      + "It will be skipped.",
                  id, sourceInfo.source.name));
          continue;
        }
        seen.put(id, sourceInfo.source.name);
        final DocumentModel entry =
            BaseSession.createEntryModel(null, schemaName, id, map, readOnlyEntries.contains(id));
        results.add(entry);
      }
    }
    return results;
  }
  @Test
  public void testReadOnlyEntryInGetEntriesResults() throws Exception {
    Map<String, String> orderBy = new HashMap<String, String>();
    orderBy.put("schema3:uid", "asc");
    DocumentModelComparator comp = new DocumentModelComparator(orderBy);

    DocumentModelList results = dir.getEntries();
    Collections.sort(results, comp);

    // by default no backing dir is readonly
    assertFalse(BaseSession.isReadOnlyEntry(results.get(0)));
    assertFalse(BaseSession.isReadOnlyEntry(results.get(1)));
    assertFalse(BaseSession.isReadOnlyEntry(results.get(2)));
    assertFalse(BaseSession.isReadOnlyEntry(results.get(3)));

    memdir1.setReadOnly(true);
    memdir2.setReadOnly(false);
    memdir3.setReadOnly(false);
    results = dir.getEntries();
    Collections.sort(results, comp);
    assertTrue(BaseSession.isReadOnlyEntry(results.get(0)));
    assertTrue(BaseSession.isReadOnlyEntry(results.get(1)));
    assertFalse(BaseSession.isReadOnlyEntry(results.get(2)));
    assertFalse(BaseSession.isReadOnlyEntry(results.get(3)));

    memdir1.setReadOnly(false);
    memdir2.setReadOnly(false);
    memdir3.setReadOnly(true);
    results = dir.getEntries();
    Collections.sort(results, comp);
    assertFalse(BaseSession.isReadOnlyEntry(results.get(0)));
    assertFalse(BaseSession.isReadOnlyEntry(results.get(1)));
    assertTrue(BaseSession.isReadOnlyEntry(results.get(2)));
    assertTrue(BaseSession.isReadOnlyEntry(results.get(3)));
  }
  @Test
  public void testReadOnlyEntryFromGetEntry() throws Exception {

    // by default no backing dir is readonly
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("1")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("2")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("3")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("4")));

    memdir1.setReadOnly(true);
    memdir2.setReadOnly(false);
    memdir3.setReadOnly(false);
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("1")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("2")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("3")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("4")));

    memdir1.setReadOnly(false);
    memdir2.setReadOnly(true);
    memdir3.setReadOnly(true);
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("1")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("2")));
    assertTrue(BaseSession.isReadOnlyEntry(dir.getEntry("3")));
    assertTrue(BaseSession.isReadOnlyEntry(dir.getEntry("4")));

    memdir1.setReadOnly(false);
    memdir2.setReadOnly(false);
    memdir3.setReadOnly(true);
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("1")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("2")));
    assertTrue(BaseSession.isReadOnlyEntry(dir.getEntry("3")));
    assertTrue(BaseSession.isReadOnlyEntry(dir.getEntry("4")));

    memdir1.setReadOnly(true);
    memdir2.setReadOnly(true);
    memdir3.setReadOnly(false);
    assertTrue(BaseSession.isReadOnlyEntry(dir.getEntry("1")));
    assertTrue(BaseSession.isReadOnlyEntry(dir.getEntry("2")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("3")));
    assertFalse(BaseSession.isReadOnlyEntry(dir.getEntry("4")));
  }