Example usage for java.util Set retainAll

List of usage examples for java.util Set retainAll

Introduction

In this page you can find the example usage for java.util Set retainAll.

Prototype

boolean retainAll(Collection<?> c);

Source Link

Document

Retains only the elements in this set that are contained in the specified collection (optional operation).

Usage

From source file:org.structr.core.property.CollectionNotionProperty.java

@Override
public SearchAttribute getSearchAttribute(SecurityContext securityContext, BooleanClause.Occur occur,
        List<T> searchValues, boolean exactMatch, final Query query) {

    final Predicate<GraphObject> predicate = query != null ? query.toPredicate() : null;
    final SourceSearchAttribute attr = new SourceSearchAttribute(occur);
    final Set<GraphObject> intersectionResult = new LinkedHashSet<>();
    boolean alreadyAdded = false;

    try {//from ww  w . j av a2s . c om

        if (searchValues != null && !searchValues.isEmpty()) {

            final PropertyKey key = notion.getPrimaryPropertyKey();
            final PropertyConverter inputConverter = key.inputConverter(securityContext);
            final List<Object> transformedValues = new LinkedList<>();
            boolean allBlank = true;

            // transform search values using input convert of notion property
            for (T searchValue : searchValues) {

                if (inputConverter != null) {

                    transformedValues.add(inputConverter.convert(searchValue));
                } else {

                    transformedValues.add(searchValue);
                }
            }

            // iterate over transformed values
            for (Object searchValue : transformedValues) {

                // check if the list contains non-empty search values
                if (StringUtils.isBlank(searchValue.toString())) {

                    continue;

                } else {

                    allBlank = false;
                }

                final App app = StructrApp.getInstance(securityContext);

                if (exactMatch) {

                    Result<AbstractNode> result = app.nodeQuery(collectionProperty.relatedType())
                            .and(notion.getPrimaryPropertyKey(), searchValue).getResult();

                    for (AbstractNode node : result.getResults()) {

                        switch (occur) {

                        case MUST:

                            if (!alreadyAdded) {

                                // the first result is the basis of all subsequent intersections
                                intersectionResult.addAll(collectionProperty.getRelatedNodesReverse(
                                        securityContext, node, declaringClass, predicate));

                                // the next additions are intersected with this one
                                alreadyAdded = true;

                            } else {

                                intersectionResult.retainAll(collectionProperty.getRelatedNodesReverse(
                                        securityContext, node, declaringClass, predicate));
                            }

                            break;

                        case SHOULD:
                            intersectionResult.addAll(collectionProperty.getRelatedNodesReverse(securityContext,
                                    node, declaringClass, predicate));
                            break;

                        case MUST_NOT:
                            break;
                        }
                    }

                } else {

                    Result<AbstractNode> result = app.nodeQuery(collectionProperty.relatedType(), false)
                            .and(notion.getPrimaryPropertyKey(), searchValue, false).getResult();

                    // loose search behaves differently, all results must be combined
                    for (AbstractNode node : result.getResults()) {

                        intersectionResult.addAll(collectionProperty.getRelatedNodesReverse(securityContext,
                                node, declaringClass, predicate));
                    }

                }
            }

            if (allBlank) {

                // experimental filter attribute that
                // removes entities with a non-empty
                // value in the given field
                return new EmptySearchAttribute(this, Collections.emptyList());

            } else {

                attr.setResult(intersectionResult);
            }

        } else {

            // experimental filter attribute that
            // removes entities with a non-empty
            // value in the given field
            return new EmptySearchAttribute(this, Collections.emptyList());

        }

    } catch (FrameworkException fex) {

        fex.printStackTrace();
    }

    return attr;
}

From source file:ca.uhn.fhir.jpa.dao.BaseFhirResourceDao.java

private Set<Long> addPredicateId(Set<Long> theExistingPids, Set<Long> thePids) {
    if (thePids == null || thePids.isEmpty()) {
        return Collections.emptySet();
    }//from   ww w  .  j  a  v  a 2s  .c  o m

    CriteriaBuilder builder = myEntityManager.getCriteriaBuilder();
    CriteriaQuery<Long> cq = builder.createQuery(Long.class);
    Root<ResourceTable> from = cq.from(ResourceTable.class);
    cq.select(from.get("myId").as(Long.class));

    Predicate typePredicate = builder.equal(from.get("myResourceType"), myResourceName);
    Predicate idPrecidate = from.get("myId").in(thePids);

    cq.where(builder.and(typePredicate, idPrecidate));

    TypedQuery<Long> q = myEntityManager.createQuery(cq);
    HashSet<Long> found = new HashSet<Long>(q.getResultList());
    if (!theExistingPids.isEmpty()) {
        theExistingPids.retainAll(found);
    }

    return found;
}

From source file:ubc.pavlab.aspiredb.server.service.QueryServiceImpl.java

@Override
@Transactional(readOnly = true)//from w  ww  . j a v  a 2 s . com
@RemoteMethod
public Map<Integer, Collection<Object>> getSubjectsVariants(Set<AspireDbFilterConfig> filters)
        throws NotLoggedInException, ExternalDependencyException {

    Map<Integer, Collection<Long>> svIds = new HashMap<>();
    Map<Integer, Collection<Object>> ret = new HashMap<>();

    Set<Long> svoIds = new HashSet<Long>();
    Set<Long> vvoIds = new HashSet<Long>();

    Set<AspireDbFilterConfig> filtersTrimmed = new HashSet<>();
    Collection<Long> subjectPhenoIds = new HashSet<>();
    Collection<Long> variantPhenoIds = new HashSet<>();

    for (AspireDbFilterConfig f : filters) {

        // treat PhenotypeFilters as a special case because calling this twice
        // is both redundant and slow (see Bug #3892)
        if (f instanceof PhenotypeFilterConfig) {
            svIds = variantDao.getSubjectVariantIdsByPhenotype((PhenotypeFilterConfig) f);

            subjectPhenoIds = svIds.get(VariantDao.SUBJECT_IDS_KEY);
            variantPhenoIds = svIds.get(VariantDao.VARIANT_IDS_KEY);

        } else {
            filtersTrimmed.add(f);
        }
    }

    StopWatch timer = new StopWatch();
    timer.start();
    List<VariantValueObject> vvos = queryVariants(filtersTrimmed).getItems();
    timer.stop();
    log.info(" query variants took " + timer);
    for (VariantValueObject v : vvos) {
        vvoIds.add(v.getId());
    }

    // need a separate call for Subject and Variants because
    // we can have a Subject without any Variants and those won't get counted!
    if (ConfigUtils.hasSubjectConfig(filters) || !ConfigUtils.hasVariantConfig(filters)) {
        timer = new StopWatch();
        timer.start();
        List<SubjectValueObject> svos = querySubjects(filtersTrimmed).getItems();
        timer.stop();
        log.info(" query subjects took " + timer);
        for (SubjectValueObject s : svos) {
            svoIds.add(s.getId());
        }
    } else {
        // if there's no Subject filter then we can just get it
        // from Variant IDs! This saves us from executing redundant queries.
        for (VariantValueObject v : vvos) {
            svoIds.add(v.getSubjectId());
        }
    }

    // intersect PhenoIds with Ids from other filters
    if (!subjectPhenoIds.isEmpty()) {
        svoIds.retainAll(subjectPhenoIds);
        vvoIds.retainAll(variantPhenoIds);
    }

    Collection<Object> retSvos = new ArrayList<>();
    for (Subject s : subjectDao.load(svoIds)) {
        retSvos.add(s.convertToValueObjectWithPhenotypes());
    }
    Collection<Object> retVvos = new ArrayList<>();
    for (Variant v : variantDao.load(vvoIds)) {
        retVvos.add(v.toValueObject());
    }

    ret.put(VariantDao.SUBJECT_IDS_KEY, retSvos);
    ret.put(VariantDao.VARIANT_IDS_KEY, retVvos);

    return ret;
}

From source file:ubc.pavlab.aspiredb.server.service.QueryServiceImpl.java

@Override
@Transactional(readOnly = true)/*from  w ww.  j  av a  2s .  c  o  m*/
@RemoteMethod
public Map<Integer, Integer> getSubjectGenes(Set<AspireDbFilterConfig> filters)
        throws NotLoggedInException, ExternalDependencyException {

    Map<Integer, Collection<Long>> svIds = new HashMap<>();
    Map<Integer, Integer> ret = new HashMap<>();

    Set<Long> svoIds = new HashSet<>();
    Set<Long> vvoIds = new HashSet<>();

    int subjectCount = 0;
    int variantCount = 0;

    Set<AspireDbFilterConfig> filtersTrimmed = new HashSet<>();
    Collection<Long> subjectPhenoIds = new HashSet<>();
    Collection<Long> variantPhenoIds = new HashSet<>();

    for (AspireDbFilterConfig f : filters) {

        // treat PhenotypeFilters as a special case because calling this twice
        // is both redundant and slow (see Bug #3892)
        if (f instanceof PhenotypeFilterConfig) {
            svIds = variantDao.getSubjectVariantIdsByPhenotype((PhenotypeFilterConfig) f);

            subjectPhenoIds = svIds.get(VariantDao.SUBJECT_IDS_KEY);
            variantPhenoIds = svIds.get(VariantDao.VARIANT_IDS_KEY);

        } else {
            filtersTrimmed.add(f);
        }
    }

    StopWatch timer = new StopWatch();
    timer.start();
    List<VariantValueObject> vvos = queryVariants(filtersTrimmed).getItems();
    timer.stop();
    log.info(" query variants took " + timer);
    for (VariantValueObject v : vvos) {
        vvoIds.add(v.getId());
    }

    // need a separate call for Subject and Variants because
    // we can have a Subject without any Variants and those won't get counted!
    if (ConfigUtils.hasSubjectConfig(filters)) {
        timer = new StopWatch();
        timer.start();
        List<SubjectValueObject> svos = querySubjects(filtersTrimmed).getItems();
        timer.stop();
        log.info(" query subjects took " + timer);
        for (SubjectValueObject s : svos) {
            svoIds.add(s.getId());
        }
    } else {
        // if there's no Subject filter then we can just get it
        // from Variant IDs! This saves us from executing redundant queries.
        for (VariantValueObject v : vvos) {
            svoIds.add(v.getSubjectId());
        }
    }

    // intersect PhenoIds with Ids from other filters
    if (!subjectPhenoIds.isEmpty()) {
        svoIds.retainAll(subjectPhenoIds);
        vvoIds.retainAll(variantPhenoIds);
    }

    subjectCount = svoIds.size();
    variantCount = vvoIds.size();

    ret.put(VariantDao.SUBJECT_IDS_KEY, subjectCount);
    ret.put(VariantDao.VARIANT_IDS_KEY, variantCount);

    return ret;
}

From source file:com.vmware.vhadoop.vhm.hadoop.HadoopAdaptor.java

@Override
/* Returns the set of active dnsNames based on input Set */
public Set<String> checkTargetTTsSuccess(String opType, Set<String> ttDnsNames, int totalTargetEnabled,
        HadoopClusterInfo cluster) {/*from  w ww  . jav a2  s.  c  o m*/
    String scriptRemoteFilePath = JOB_TRACKER_DEFAULT_SCRIPT_DEST_PATH + JOB_TRACKER_CHECK_SCRIPT_FILE_NAME;
    String listRemoteFilePath = null;
    String opDesc = "checkTargetTTsSuccess";

    if (ttDnsNames == null) {
        _log.warning("No valid TT names provided");
        return null;
    }

    /* We don't expect null or empty values, but weed out anyway */
    ttDnsNames.remove(null);
    ttDnsNames.remove("");
    if (ttDnsNames.size() == 0) {
        _log.warning("No valid TT names provided");
        return null;
    }

    _log.log(Level.INFO, "Affected TTs: " + ttDnsNames);

    setErrorParamsForCommand(cluster, opDesc, scriptRemoteFilePath, listRemoteFilePath);

    int iterations = 0;
    CompoundStatus getActiveStatus = null;
    int rc = UNKNOWN_ERROR;
    Set<String> allActiveTTs = null;
    long lastCheckAttemptTime = Long.MAX_VALUE;
    do {
        if (iterations > 0) {
            /* 1141429: Ensure that if the script fails, there is a minimum wait before the next retry attempt */
            long millisSinceLastCheck = (System.currentTimeMillis() - lastCheckAttemptTime);
            long underWaitMillis = JOB_TRACKER_CHECK_SCRIPT_MIN_RETRY_MILLIS - millisSinceLastCheck;
            if (underWaitMillis > 0) {
                try {
                    _log.fine("Sleeping for underWaitMillis = " + underWaitMillis);
                    Thread.sleep(underWaitMillis);
                } catch (InterruptedException e) {
                }
            }
            _log.log(Level.INFO, "Target TTs not yet achieved...checking again - " + iterations);
            _log.log(Level.INFO, "Affected TTs: " + ttDnsNames);
        }

        getActiveStatus = new CompoundStatus(ACTIVE_TTS_STATUS_KEY);

        lastCheckAttemptTime = System.currentTimeMillis();
        allActiveTTs = getActiveTTs(cluster, totalTargetEnabled, getActiveStatus);

        //Declare success as long as the we manage to de/recommission only the TTs we set out to handle (rather than checking correctness for all TTs)
        if ((allActiveTTs != null) && ((opType.equals("Recommission") && allActiveTTs.containsAll(ttDnsNames))
                || (opType.equals("Decommission") && ttDnsNames.retainAll(allActiveTTs)
                        && ttDnsNames.isEmpty()))) {
            _log.log(Level.INFO, "All selected TTs correctly %sed", opType.toLowerCase());
            rc = SUCCESS;
            break;
        }

        /* If there was an error reported by getActiveTTs... */
        TaskStatus taskStatus = getActiveStatus.getFirstFailure(STATUS_INTERPRET_ERROR_CODE);
        if (taskStatus != null) {
            rc = taskStatus.getErrorCode();
        } else {
            /*
             * JG: Sometimes we don't know the hostnames (e.g., localhost); in these cases as long as the check script returns success based
             * on target #TTs we are good.
             * TODO: Change check script to return success if #newly added + #current_enabled is met rather than target #TTs is met. This is
             * to address scenarios where there is a mismatch (#Active TTs != #poweredOn VMs) to begin with...
             * CHANGED: We have changed the time at which this function is invoked -- it gets invoked only when dns/hostnames are available.
             * So we no longer have this issue of not knowing hostnames and still meeting target #TTs. Our only successful exit is when the
             * TTs that have been explicitly asked to be checked, have been correctly de/recommissioned.
             *
             * rc = SUCCESS; //Note: removing this
             *
             * We also notice that in this case, where #Active TTs matches target, but all the requested TTs haven't been de/recommissioned yet,
             * the check script returns immediately (because it only looks for a match of these values, which is true here). So we recompute
             * target TTs based on latest information to essentially put back the delay...
             */

            Set<String> deltaTTs = new HashSet<String>(ttDnsNames);
            if (opType.equals("Recommission")) {
                deltaTTs.removeAll(allActiveTTs); //get TTs that haven't been recommissioned yet...
                totalTargetEnabled = allActiveTTs.size() + deltaTTs.size();
            } else { //optype = Decommission
                deltaTTs.retainAll(allActiveTTs); //get TTs that haven't been decommissioned yet...
                totalTargetEnabled = allActiveTTs.size() - deltaTTs.size();
            }

            _log.log(Level.INFO,
                    "Even though #ActiveTTs = #TargetTTs, not all requested TTs have been "
                            + opType.toLowerCase() + "ed yet - Trying again with updated target: "
                            + totalTargetEnabled);
        }

        /* Break out if there is an error other than the ones we expect to be resolved in a subsequent invocation of the check script */
        if (rc != ERROR_FEWER_TTS && rc != ERROR_EXCESS_TTS && rc != UNKNOWN_ERROR) {
            break;
        }
    } while (iterations++ < ACTIVE_TASK_TRACKERS_CHECK_RETRY_ITERATIONS);

    getCompoundStatus().addStatus(_errorCodes.interpretErrorCode(_log, rc, getErrorParamValues(cluster)));
    if (rc != SUCCESS) {
        getActiveStatus.registerTaskFailed(false, "Check Test Failed");
        getCompoundStatus().addStatus(getActiveStatus);
    }

    return allActiveTTs;
}

From source file:com.jeans.iservlet.service.asset.impl.AssetServiceImpl.java

@Override
@Transactional(readOnly = true)/*from  w  ww.j  a  v a2 s .  co  m*/
public Set<Byte> checkNextStates(Set<Long> ids, byte type) {
    Set<Byte> states = new HashSet<Byte>();
    if (ids.size() == 0) {
        return states;
    }

    states.add(AssetConstants.IN_USE);
    states.add(AssetConstants.IDLE);
    states.add(AssetConstants.DISUSE);
    if (type == AssetConstants.HARDWARE_ASSET) {
        states.add(AssetConstants.FIXING);
        states.add(AssetConstants.ELIMINATED);
    }

    // ?
    Set<Byte> n_h_iu = new HashSet<Byte>();
    Set<Byte> n_h_id = new HashSet<Byte>();
    Set<Byte> n_h_fx = new HashSet<Byte>();
    Set<Byte> n_h_du = new HashSet<Byte>();
    Set<Byte> n_s_iu = new HashSet<Byte>();
    Set<Byte> n_s_id = new HashSet<Byte>();

    n_h_iu.add(AssetConstants.IN_USE);
    n_h_iu.add(AssetConstants.IDLE);
    n_h_iu.add(AssetConstants.FIXING);

    n_h_id.add(AssetConstants.IN_USE);
    n_h_id.add(AssetConstants.FIXING);
    n_h_id.add(AssetConstants.DISUSE);

    n_h_fx.add(AssetConstants.IN_USE);
    n_h_fx.add(AssetConstants.IDLE);
    n_h_fx.add(AssetConstants.DISUSE);

    n_h_du.add(AssetConstants.ELIMINATED);

    n_s_iu.add(AssetConstants.IDLE);
    n_s_iu.add(AssetConstants.DISUSE);

    n_s_id.add(AssetConstants.IN_USE);
    n_s_id.add(AssetConstants.DISUSE);

    List<Asset> assets = loadAssets(ids, type);
    if (type == AssetConstants.HARDWARE_ASSET) {
        /*
         * (???)  -> //  -> //  -> //?  -> ? ? -> null
         */
        for (Asset asset : assets) {
            byte oldState = asset.getState();
            if (oldState == AssetConstants.ELIMINATED) {
                // ????
                states.clear();
                break;
            } else {
                if (states.size() == 0) {
                    // ?????
                    break;
                } else {
                    switch (oldState) {
                    case AssetConstants.IN_USE:
                        states.retainAll(n_h_iu);
                        break;
                    case AssetConstants.IDLE:
                        states.retainAll(n_h_id);
                        break;
                    case AssetConstants.FIXING:
                        states.retainAll(n_h_fx);
                        break;
                    case AssetConstants.DISUSE:
                        states.retainAll(n_h_du);
                    }
                }
            }
        }
    } else {
        /*
         *   -> /  -> /  -> null
         */
        for (Asset asset : assets) {
            byte oldState = asset.getState();
            if (oldState == AssetConstants.DISUSE) {
                // ???
                states.clear();
                break;
            } else {
                if (states.size() == 1) {
                    // ?????????????
                    continue;
                } else {
                    if (oldState == AssetConstants.IN_USE) {
                        states.retainAll(n_s_iu);
                    } else {
                        states.retainAll(n_s_id);
                    }
                }
            }
        }
    }
    return states;
}

From source file:org.alfresco.repo.security.sync.ChainingUserRegistrySynchronizer.java

/**
 * Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling
 * deletions./*from w  w w  .j  a v a2 s  .c  o  m*/
 * 
 * @param zone
 *            the zone id. This identifier is used to tag all created groups and users, so that in the future we can
 *            tell those that have been deleted from the registry.
 * @param userRegistry
 *            the user registry for the zone.
 * @param forceUpdate
 *            Should the complete set of users and groups be updated / created locally or just those known to have
 *            changed since the last sync? When <code>true</code> then <i>all</i> users and groups are queried from
 *            the user registry and updated locally. When <code>false</code> then each source is only queried for
 *            those users and groups modified since the most recent modification date of all the objects last
 *            queried from that same source.
 * @param isFullSync
 *            Should a complete set of user and group IDs be queried from the user registries in order to determine
 *            deletions? This parameter is independent of <code>force</code> as a separate query is run to process
 *            updates.
 * @param splitTxns
 *            Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
 *            <code>true</code>, users and groups are created/updated in batches for increased performance. If
 *            <code>false</code>, all users and groups are processed in the current transaction. This is required if
 *            calling synchronously (e.g. in response to an authentication event in the same transaction).
 * @param visitedZoneIds
 *            the set of zone ids already processed. These zones have precedence over the current zone when it comes
 *            to group name 'collisions'. If a user or group is queried that already exists locally but is tagged
 *            with one of the zones in this set, then it will be ignored as this zone has lower priority.
 * @param allZoneIds
 *            the set of all zone ids in the authentication chain. Helps us work out whether the zone information
 *            recorded against a user or group is invalid for the current authentication chain and whether the user
 *            or group needs to be 're-zoned'.
 */
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate,
        boolean isFullSync, boolean splitTxns, final Set<String> visitedZoneIds, final Set<String> allZoneIds) {
    // Create a prefixed zone ID for use with the authority service
    final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;

    // Batch Process Names
    final String reservedBatchProcessNames[] = { SyncProcess.GROUP_ANALYSIS.getTitle(zone),
            SyncProcess.USER_CREATION.getTitle(zone), SyncProcess.MISSING_AUTHORITY.getTitle(zone),
            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
            SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
            SyncProcess.PERSON_ASSOCIATION.getTitle(zone), SyncProcess.AUTHORITY_DELETION.getTitle(zone) };

    notifySyncDirectoryStart(zone, reservedBatchProcessNames);

    // Ensure that the zoneId exists before multiple threads start using it
    this.transactionService.getRetryingTransactionHelper()
            .doInTransaction(new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable {
                    authorityService.getOrCreateZone(zoneId);
                    return null;
                }
            }, false, splitTxns);

    // The set of zones we associate with new objects (default plus registry specific)
    final Set<String> zoneSet = getZones(zoneId);

    long lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                    splitTxns);
    Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);

    if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            ChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all groups from user registry '" + zone + "'");
        } else {
            ChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // First, analyze the group structure. Create maps of authorities to their parents for associations to create
    // and delete. Also deal with 'overlaps' with other zones in the authentication chain.
    final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.GROUP_ANALYSIS.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher,
            ChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
        private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
        private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
        private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
        private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
        private List<String> personsProcessed = new LinkedList<String>();
        private Set<String> allZonePersons = Collections.emptySet();
        private Set<String> deletionCandidates;

        private long latestTime;

        public Analyzer(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public Set<String> getDeletionCandidates() {
            return this.deletionCandidates;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription group) throws Throwable {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService
                    .getShortName(groupName);
            Set<String> groupZones = ChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(groupName);

            if (groupZones == null) {
                // The group did not exist at all
                updateGroup(group, false);
            } else {
                // Check whether the group is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(groupZones);
                intersection.retainAll(allZoneIds);
                // Check whether the group is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);

                if (groupZones.contains(zoneId)) {
                    // The group already existed in this zone: update the group
                    updateGroup(group, true);
                } else if (!visited.isEmpty()) {
                    // A group that exists in a different zone with higher precedence
                    return;
                } else if (!allowDeletions || intersection.isEmpty()) {
                    // Deletions are disallowed or the group exists, but not in a zone that's in the authentication
                    // chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
                                + "'. This group will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(groupName, groupZones, zoneSet);

                    // The group now exists in this zone: update the group
                    updateGroup(group, true);
                } else {
                    // The group existed, but in a zone with lower precedence
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '"
                                + groupShortName
                                + "'. This group was previously created through synchronization with a lower priority user registry.");
                    }
                    ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);

                    // create the group
                    updateGroup(group, false);
                }
            }

            synchronized (this) {
                // Maintain the last modified date
                Date groupLastModified = group.getLastModified();
                if (groupLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
                }
            }
        }

        // Recursively walks and caches the authorities relating to and from this group so that we can later detect potential cycles
        private Set<String> getContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // First, recurse to the parent most authorities
            for (String parent : ChainingUserRegistrySynchronizer.this.authorityService
                    .getContainingAuthorities(null, groupName, true)) {
                getContainedAuthorities(parent);
            }

            // Now descend on unprocessed parents.
            return cacheContainedAuthorities(groupName);
        }

        private Set<String> cacheContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // Descend on unprocessed parents.
            children = ChainingUserRegistrySynchronizer.this.authorityService.getContainedAuthorities(null,
                    groupName, true);
            this.finalGroupChildAssocs.put(groupName, children);

            for (String child : children) {
                if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
                    cacheContainedAuthorities(child);
                }
            }
            return children;
        }

        private synchronized void updateGroup(NodeDescription group, boolean existed) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
            if (groupDisplayName == null) {
                groupDisplayName = ChainingUserRegistrySynchronizer.this.authorityService
                        .getShortName(groupName);
            }

            // Divide the child associations into person and group associations, dealing with case sensitivity
            Set<String> newChildPersons = newPersonSet();
            Set<String> newChildGroups = new TreeSet<String>();

            for (String child : group.getChildAssociations()) {
                if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                    newChildPersons.add(child);
                } else {
                    newChildGroups.add(child);
                }
            }

            // Account for differences if already existing
            if (existed) {
                // Update the display name now
                ChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName,
                        groupDisplayName);

                // Work out the association differences
                for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
                    if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                        if (!newChildPersons.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    } else {
                        if (!newChildGroups.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    }
                }
            }
            // Mark as created if new
            else {
                // Make sure each group to be created features in the association deletion map (as these are handled in the same phase)
                recordParentAssociationDeletion(groupName, null);
                this.groupsToCreate.put(groupName, groupDisplayName);
            }

            // Create new associations
            for (String child : newChildPersons) {
                // Make sure each person with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
            for (String child : newChildGroups) {
                // Make sure each group with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
        }

        private void recordParentAssociationDeletion(String child, String parent) {
            Map<String, Set<String>> parentAssocs;
            if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                parentAssocs = this.personParentAssocsToDelete;
            } else {
                // Reflect the change in the map of final group associations (for cycle detection later)
                parentAssocs = this.groupParentAssocsToDelete;
                if (parent != null) {
                    Set<String> children = this.finalGroupChildAssocs.get(parent);
                    children.remove(child);
                }
            }
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void recordParentAssociationCreation(String child, String parent) {
            Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER
                    ? this.personParentAssocsToCreate
                    : this.groupParentAssocsToCreate;
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void validateGroupParentAssocsToCreate() {
            Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String group = entry.getKey();
                Set<String> parents = entry.getValue();
                Deque<String> visited = new LinkedList<String>();
                Iterator<String> j = parents.iterator();
                while (j.hasNext()) {
                    String parent = j.next();
                    visited.add(parent);
                    if (validateAuthorityChildren(visited, group)) {
                        // The association validated - commit it
                        Set<String> children = finalGroupChildAssocs.get(parent);
                        if (children == null) {
                            children = new TreeSet<String>();
                            finalGroupChildAssocs.put(parent, children);
                        }
                        children.add(group);
                    } else {
                        // The association did not validate - prune it out
                        if (logger.isWarnEnabled()) {
                            ChainingUserRegistrySynchronizer.logger.warn("Not adding group '"
                                    + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(group)
                                    + "' to group '" + ChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent)
                                    + "' as this creates a cyclic relationship");
                        }
                        j.remove();
                    }
                    visited.removeLast();
                }
                if (parents.isEmpty()) {
                    i.remove();
                }
            }

            // Sort the group associations in parent-first order (root groups first) to minimize reindexing overhead
            Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(
                    this.groupParentAssocsToCreate.size() * 2);
            Deque<String> visited = new LinkedList<String>();
            for (String authority : this.groupParentAssocsToCreate.keySet()) {
                visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate,
                        sortedGroupAssociations);
            }

            this.groupParentAssocsToCreate = sortedGroupAssociations;
        }

        private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                return true;
            }
            if (visited.contains(authority)) {
                return false;
            }
            visited.add(authority);
            try {
                Set<String> children = this.finalGroupChildAssocs.get(authority);
                if (children != null) {
                    for (String child : children) {
                        if (!validateAuthorityChildren(visited, child)) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        /**
         * Visits the given authority by recursively visiting its parents in associationsOld and then adding the
         * authority to associationsNew. Used to sort associationsOld into 'parent-first' order to minimize
         * reindexing overhead.
         * 
         * @param visited
         *            The ancestors that form the path to the authority to visit. Allows detection of cyclic child
         *            associations.
         * @param authority
         *            the authority to visit
         * @param associationsOld
         *            the association map to sort
         * @param associationsNew
         *            the association map to add to in parent-first order
         */
        private boolean visitGroupParentAssocs(Deque<String> visited, String authority,
                Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
            if (visited.contains(authority)) {
                // Prevent cyclic paths (Shouldn't happen as we've already validated)
                return false;
            }
            visited.add(authority);
            try {
                if (!associationsNew.containsKey(authority)) {
                    Set<String> oldParents = associationsOld.get(authority);
                    if (oldParents != null) {
                        Set<String> newParents = new TreeSet<String>();

                        for (String parent : oldParents) {
                            if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
                                newParents.add(parent);
                            }
                        }
                        associationsNew.put(authority, newParents);
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        private Set<String> newPersonSet() {
            return ChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeSet<String>()
                    : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        }

        private Map<String, Set<String>> newPersonMap() {
            return ChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeMap<String, Set<String>>()
                    : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
        }

        private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
            Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
            StringBuilder groupList = null;
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String child = entry.getKey();
                if (!toRetain.contains(child)) {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        if (groupList == null) {
                            groupList = new StringBuilder(1024);
                        } else {
                            groupList.setLength(0);
                        }
                        for (String parent : entry.getValue()) {
                            if (groupList.length() > 0) {
                                groupList.append(", ");
                            }
                            groupList.append('\'').append(
                                    ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent))
                                    .append('\'');

                        }
                        ChainingUserRegistrySynchronizer.logger.debug("Ignoring non-existent member '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(child)
                                + "' in groups {" + groupList.toString() + "}");
                    }
                    i.remove();
                }
            }
        }

        private void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
            // MNT-12454 fix. If syncDelete is false, there is no need to pull all users and all groups from LDAP during the full synchronization.
            if ((syncDelete || !groupsToCreate.isEmpty())
                    && (isFullSync || !this.groupParentAssocsToDelete.isEmpty())) {
                final Set<String> allZonePersons = newPersonSet();
                final Set<String> allZoneGroups = new TreeSet<String>();

                // Add in current set of known authorities
                ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                        .doInTransaction(new RetryingTransactionCallback<Void>() {
                            public Void execute() throws Throwable {
                                allZonePersons.addAll(ChainingUserRegistrySynchronizer.this.authorityService
                                        .getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
                                allZoneGroups.addAll(ChainingUserRegistrySynchronizer.this.authorityService
                                        .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
                                return null;
                            }
                        }, true, splitTxns);

                allZoneGroups.addAll(this.groupsToCreate.keySet());

                // Prune our set of authorities according to deletions
                if (isFullSync) {
                    final Set<String> personDeletionCandidates = newPersonSet();
                    personDeletionCandidates.addAll(allZonePersons);

                    final Set<String> groupDeletionCandidates = new TreeSet<String>();
                    groupDeletionCandidates.addAll(allZoneGroups);

                    this.deletionCandidates = new TreeSet<String>();

                    for (String person : userRegistry.getPersonNames()) {
                        personDeletionCandidates.remove(person);
                    }

                    for (String group : userRegistry.getGroupNames()) {
                        groupDeletionCandidates.remove(group);
                    }

                    this.deletionCandidates = new TreeSet<String>();
                    this.deletionCandidates.addAll(personDeletionCandidates);
                    this.deletionCandidates.addAll(groupDeletionCandidates);

                    if (allowDeletions) {
                        allZonePersons.removeAll(personDeletionCandidates);
                        allZoneGroups.removeAll(groupDeletionCandidates);
                    } else {
                        // Complete association deletion information by scanning deleted groups
                        BatchProcessor<String> groupScanner = new BatchProcessor<String>(
                                zone + " Missing Authority Scanning",
                                ChainingUserRegistrySynchronizer.this.transactionService
                                        .getRetryingTransactionHelper(),
                                this.deletionCandidates, ChainingUserRegistrySynchronizer.this.workerThreads,
                                20, ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                                ChainingUserRegistrySynchronizer.logger,
                                ChainingUserRegistrySynchronizer.this.loggingInterval);
                        groupScanner.process(new BaseBatchProcessWorker<String>() {

                            @Override
                            public String getIdentifier(String entry) {
                                return entry;
                            }

                            @Override
                            public void process(String authority) throws Throwable {
                                //MNT-12454 fix. Modifies an authority's zone. Move authority from AUTH.EXT.LDAP1 to AUTH.ALF.
                                updateAuthorityZones(authority, Collections.singleton(zoneId),
                                        Collections.singleton(AuthorityService.ZONE_AUTH_ALFRESCO));
                            }
                        }, splitTxns);
                    }

                }

                // Prune the group associations now that we have complete information
                this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
                logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
                this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);

                // Pruning person associations will have to wait until we have passed over all persons and built up
                // this set
                this.allZonePersons = allZonePersons;

                if (!this.groupParentAssocsToDelete.isEmpty()) {
                    // Create/update the groups and delete parent associations to be deleted
                    // Batch 4 Group Creation and Association Deletion
                    BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
                            ChainingUserRegistrySynchronizer.this.transactionService
                                    .getRetryingTransactionHelper(),
                            this.groupParentAssocsToDelete.entrySet(),
                            ChainingUserRegistrySynchronizer.this.workerThreads, 20,
                            ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                            ChainingUserRegistrySynchronizer.logger,
                            ChainingUserRegistrySynchronizer.this.loggingInterval);
                    groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                        public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                            return entry.getKey() + " " + entry.getValue();
                        }

                        public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                            String child = entry.getKey();

                            String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
                            if (groupDisplayName != null) {
                                String groupShortName = ChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(child);
                                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                                    ChainingUserRegistrySynchronizer.logger
                                            .debug("Creating group '" + groupShortName + "'");
                                }
                                // create the group
                                ChainingUserRegistrySynchronizer.this.authorityService.createAuthority(
                                        AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName,
                                        zoneSet);
                            } else {
                                // Maintain association deletions now. The creations will have to be done later once
                                // we have performed all the deletions in order to avoid creating cycles
                                maintainAssociationDeletions(child);
                            }
                        }
                    }, splitTxns);
                }
            }
        }

        private void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
            // First validate the group associations to be created for potential cycles. Remove any offending association
            validateGroupParentAssocsToCreate();

            // Now go ahead and create the group associations
            if (!this.groupParentAssocsToCreate.isEmpty()) {
                // Batch 5 Group Association Creation
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
                        ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(),
                        this.groupParentAssocsToCreate.entrySet(),
                        ChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        ChainingUserRegistrySynchronizer.logger,
                        ChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }

            // Remove all the associations we have already dealt with
            this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);

            // Filter out associations to authorities that simply can't exist (and log if debugging is enabled)
            logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);

            // Update associations to persons not updated themselves
            if (!this.personParentAssocsToDelete.isEmpty()) {
                // Batch 6 Person Association
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.PERSON_ASSOCIATION.getTitle(zone),
                        ChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper(),
                        this.personParentAssocsToDelete.entrySet(),
                        ChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        ChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        ChainingUserRegistrySynchronizer.logger,
                        ChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationDeletions(entry.getKey());
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }
        }

        private void maintainAssociationDeletions(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName)
                    : this.groupParentAssocsToDelete.get(authorityName);
            if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
                for (String parent : parentsToDelete) {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.debug("Removing '"
                                + ChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' from group '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(parent)
                                + "'");
                    }
                    ChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent,
                            authorityName);
                }
            }

        }

        private void maintainAssociationCreations(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName)
                    : this.groupParentAssocsToCreate.get(authorityName);
            if (parents != null && !parents.isEmpty()) {
                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    for (String groupName : parents) {
                        ChainingUserRegistrySynchronizer.logger.debug("Adding '"
                                + ChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' to group '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(groupName)
                                + "'");
                    }
                }
                try {
                    ChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents, authorityName);
                } catch (UnknownAuthorityException e) {
                    // Let's force a transaction retry if a parent doesn't exist. It may be because we are
                    // waiting for another worker thread to create it
                    throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
                } catch (InvalidNodeRefException e) {
                    // Another thread may have written the node, but it is not visible to this transaction
                    // See: ALF-5471: 'authorityMigration' patch can report 'Node does not exist'
                    throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
                }
            }
            // Remember that this person's associations have been maintained
            if (isPerson) {
                synchronized (this) {
                    this.personsProcessed.add(authorityName);
                }
            }
        }
    } // end of Analyzer class

    // Run the first process the Group Analyzer
    final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
    int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);

    groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);

    // Process persons and their parent associations

    lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                    splitTxns);
    lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
    if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            ChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all users from user registry '" + zone + "'");
        } else {
            ChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // User Creation and Association
    final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.USER_CREATION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher,
            ChainingUserRegistrySynchronizer.logger, this.loggingInterval);

    final UserRegistry userRegistryFinalRef = userRegistry;

    class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
        private long latestTime;

        public PersonWorker(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription person) throws Throwable {
            // Make a mutable copy of the person properties, since they get written back to by person service
            HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(
                    person.getProperties());
            String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim();
            personProperties.put(ContentModel.PROP_USERNAME, personName);

            if (Boolean.parseBoolean(ChainingUserRegistrySynchronizer.this.externalUserControl)
                    && ChainingUserRegistrySynchronizer.this.externalUserControlSubsystemName.equals(zone)
                    && userRegistryFinalRef instanceof LDAPUserRegistry) {
                try {
                    LDAPUserRegistry ldapUserRegistry = (LDAPUserRegistry) userRegistryFinalRef;

                    if (ldapUserRegistry.getUserAccountStatusInterpreter() != null) {
                        QName propertyNameToCheck = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI,
                                "userAccountStatusProperty");

                        if (personProperties.get(propertyNameToCheck) != null
                                || ldapUserRegistry.getUserAccountStatusInterpreter().acceptsNullArgument()) {
                            boolean isUserAccountDisabled = ldapUserRegistry.getUserAccountStatusInterpreter()
                                    .isUserAccountDisabled(personProperties.get(propertyNameToCheck));

                            personProperties.put(ContentModel.PROP_ENABLED, !isUserAccountDisabled);
                        }
                    }
                } catch (IllegalArgumentException iae) {
                    // Can be thrown by certain implementations of AbstractDirectoryServiceUserAccountStatusInterpreter;
                    // We'll just log it.
                    ChainingUserRegistrySynchronizer.logger.debug(iae.getMessage(), iae);
                }
            }

            // for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback
            nameChecker.evaluate(personName);
            Set<String> zones = ChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(personName);
            if (zones == null) {
                // The person did not exist at all
                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    ChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
                }
                ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
            } else if (zones.contains(zoneId)) {
                // The person already existed in this zone: update the person
                if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    ChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
                }
                ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                        personProperties, false);
            } else {
                // Check whether the user is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(zones);
                intersection.retainAll(allZoneIds);
                // Check whether the user is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);
                if (visited.size() > 0) {
                    // A person that exists in a different zone with higher precedence - ignore
                    return;
                }

                else if (!allowDeletions || intersection.isEmpty()) {
                    // The person exists, but in a different zone. Either deletions are disallowed or the zone is
                    // not in the authentication chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
                                + "'. This user will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(personName, zones, zoneSet);
                    ChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                            personProperties, false);
                } else {
                    // The person existed, but in a zone with lower precedence
                    if (ChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '" + personName
                                + "'. This user was previously created through synchronization with a lower priority user registry.");
                    }
                    ChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
                    ChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties, zoneSet);
                }
            }

            // Maintain association deletions and creations in one shot (safe to do this with persons as we can't
            // create cycles)
            groupAnalyzer.maintainAssociationDeletions(personName);
            groupAnalyzer.maintainAssociationCreations(personName);

            synchronized (this) {
                // Maintain the last modified date
                Date personLastModified = person.getLastModified();
                if (personLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
                }
            }
        }
    }

    PersonWorker persons = new PersonWorker(lastModifiedMillis);
    int personProcessedCount = personProcessor.process(persons, splitTxns);

    // Process those associations to persons who themselves have not been updated
    groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);

    // Only now that the whole tree has been processed is it safe to persist the last modified dates
    long latestTime = groupAnalyzer.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }
    latestTime = persons.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(ChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }

    // Delete authorities if we have complete information for the zone
    Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
    if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
        // Batch 7 Authority Deletion
        BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
                SyncProcess.AUTHORITY_DELETION.getTitle(zone),
                this.transactionService.getRetryingTransactionHelper(), deletionCandidates, this.workerThreads,
                10, this.applicationEventPublisher, ChainingUserRegistrySynchronizer.logger,
                this.loggingInterval);
        class AuthorityDeleter extends BaseBatchProcessWorker<String> {
            private int personProcessedCount;
            private int groupProcessedCount;

            public int getPersonProcessedCount() {
                return this.personProcessedCount;
            }

            public int getGroupProcessedCount() {
                return this.groupProcessedCount;
            }

            public String getIdentifier(String entry) {
                return entry;
            }

            public void process(String authority) throws Throwable {
                if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.debug("Deleting user '" + authority + "'");
                    }
                    ChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
                    synchronized (this) {
                        this.personProcessedCount++;
                    }
                } else {
                    if (ChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        ChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
                                + ChainingUserRegistrySynchronizer.this.authorityService.getShortName(authority)
                                + "'");
                    }
                    ChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
                    synchronized (this) {
                        this.groupProcessedCount++;
                    }
                }
            }
        }
        AuthorityDeleter authorityDeleter = new AuthorityDeleter();
        authorityDeletionProcessor.process(authorityDeleter, splitTxns);
        groupProcessedCount += authorityDeleter.getGroupProcessedCount();
        personProcessedCount += authorityDeleter.getPersonProcessedCount();
    }

    // Remember we have visited this zone
    visitedZoneIds.add(zoneId);

    Object statusParams[] = { personProcessedCount, groupProcessedCount };
    final String statusMessage = I18NUtil.getMessage("synchronization.summary.status", statusParams);

    if (ChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        ChainingUserRegistrySynchronizer.logger
                .info("Finished synchronizing users and groups with user registry '" + zone + "'");
        ChainingUserRegistrySynchronizer.logger.info(statusMessage);
    }

    notifySyncDirectoryEnd(zone, statusMessage);

}

From source file:org.alfresco.repo.security.sync.TenantChainingUserRegistrySynchronizer.java

/**
 * Synchronizes local groups and users with a {@link UserRegistry} for a
 * particular zone, optionally handling deletions.
 * /* www . ja  va2 s . com*/
 * @param zone
 *            the zone id. This identifier is used to tag all created groups
 *            and users, so that in the future we can tell those that have
 *            been deleted from the registry.
 * @param userRegistry
 *            the user registry for the zone.
 * @param forceUpdate
 *            Should the complete set of users and groups be updated /
 *            created locally or just those known to have changed since the
 *            last sync? When <code>true</code> then <i>all</i> users and
 *            groups are queried from the user registry and updated locally.
 *            When <code>false</code> then each source is only queried for
 *            those users and groups modified since the most recent
 *            modification date of all the objects last queried from that
 *            same source.
 * @param isFullSync
 *            Should a complete set of user and group IDs be queried from
 *            the user registries in order to determine deletions? This
 *            parameter is independent of <code>force</code> as a separate
 *            query is run to process updates.
 * @param splitTxns
 *            Can the modifications to Alfresco be split across multiple
 *            transactions for maximum performance? If <code>true</code>,
 *            users and groups are created/updated in batches for increased
 *            performance. If <code>false</code>, all users and groups are
 *            processed in the current transaction. This is required if
 *            calling synchronously (e.g. in response to an authentication
 *            event in the same transaction).
 * @param visitedZoneIds
 *            the set of zone ids already processed. These zones have
 *            precedence over the current zone when it comes to group name
 *            'collisions'. If a user or group is queried that already
 *            exists locally but is tagged with one of the zones in this
 *            set, then it will be ignored as this zone has lower priority.
 * @param allZoneIds
 *            the set of all zone ids in the authentication chain. Helps us
 *            work out whether the zone information recorded against a user
 *            or group is invalid for the current authentication chain and
 *            whether the user or group needs to be 're-zoned'.
 */
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate,
        boolean isFullSync, boolean splitTxns, final Set<String> visitedZoneIds, final Set<String> allZoneIds) {
    // Create a prefixed zone ID for use with the authority service
    final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;

    // Ensure that the zoneId exists before multiple threads start using it
    this.transactionService.getRetryingTransactionHelper()
            .doInTransaction(new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable {
                    authorityService.getOrCreateZone(zoneId);
                    return null;
                }
            }, false, splitTxns);

    // The set of zones we associate with new objects (default plus registry
    // specific)
    final Set<String> zoneSet = getZones(zoneId);

    long lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);

    if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            TenantChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all groups from user registry '" + zone + "'");
        } else {
            TenantChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // First, analyze the group structure. Create maps of authorities to
    // their parents for associations to create
    // and delete. Also deal with 'overlaps' with other zones in the
    // authentication chain.
    final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
            zone + " Group Analysis", this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher,
            TenantChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
        private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
        private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
        private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
        private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
        private List<String> personsProcessed = new LinkedList<String>();
        private Set<String> allZonePersons = Collections.emptySet();
        private Set<String> deletionCandidates;

        private long latestTime;

        public Analyzer(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public Set<String> getDeletionCandidates() {
            return this.deletionCandidates;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(final NodeDescription group) throws Throwable {
            PropertyMap groupProperties = group.getProperties();
            String tenantDomain = (String) groupProperties.get(ContentModel.PROP_ORGANIZATION);
            logger.debug("Process group: " + groupProperties.get(ContentModel.PROP_AUTHORITY_NAME)
                    + (tenantDomain == null ? "" : ("/" + tenantDomain)));

            if (tenantDomain != null) {
                if (!isTenantEnabled(tenantDomain)) {
                    return;
                }
                AuthenticationUtil.runAs(new RunAsWork<Void>() {
                    @Override
                    public Void doWork() throws Exception {
                        processInTenantMode(group);
                        return null;
                    }
                }, SEIPTenantIntegration.getSystemUserByTenantId(tenantDomain));
            } else {
                processInTenantMode(group);
            }
        }

        private void processInTenantMode(NodeDescription group) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupShortName = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getShortName(groupName);
            Set<String> groupZones = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(groupName);

            if (groupZones == null) {
                // The group did not exist at all
                updateGroup(group, false);
            } else {
                // Check whether the group is in any of the authentication
                // chain zones
                Set<String> intersection = new TreeSet<String>(groupZones);
                intersection.retainAll(allZoneIds);
                // Check whether the group is in any of the higher priority
                // authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);

                if (groupZones.contains(zoneId)) {
                    // The group already existed in this zone: update the
                    // group
                    updateGroup(group, true);
                } else if (!visited.isEmpty()) {
                    // A group that exists in a different zone with higher
                    // precedence
                    return;
                } else if (!allowDeletions || intersection.isEmpty()) {
                    // Deletions are disallowed or the group exists, but not
                    // in a zone that's in the authentication
                    // chain. May be due to upgrade or zone changes. Let's
                    // re-zone them
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
                                + "'. This group will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(groupName, groupZones, zoneSet);

                    // The group now exists in this zone: update the group
                    updateGroup(group, true);
                } else {
                    // The group existed, but in a zone with lower
                    // precedence
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '"
                                + groupShortName
                                + "'. This group was previously created through synchronization with a lower priority user registry.");
                    }
                    TenantChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);

                    // create the group
                    updateGroup(group, false);
                }
            }

            synchronized (this) {
                // Maintain the last modified date
                Date groupLastModified = group.getLastModified();
                if (groupLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
                }
            }
        }

        // Recursively walks and caches the authorities relating to and from
        // this group so that we can later detect potential cycles
        private Set<String> getContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // First, recurse to the parent most authorities
            for (String parent : TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getContainingAuthorities(null, groupName, true)) {
                getContainedAuthorities(parent);
            }

            // Now descend on unprocessed parents.
            return cacheContainedAuthorities(groupName);
        }

        private Set<String> cacheContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // Descend on unprocessed parents.
            children = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getContainedAuthorities(null, groupName, true);
            this.finalGroupChildAssocs.put(groupName, children);

            for (String child : children) {
                if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
                    cacheContainedAuthorities(child);
                }
            }
            return children;
        }

        private synchronized void updateGroup(NodeDescription group, boolean existed) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
            if (groupDisplayName == null) {
                groupDisplayName = TenantChainingUserRegistrySynchronizer.this.authorityService
                        .getShortName(groupName);
            }

            // Divide the child associations into person and group
            // associations, dealing with case sensitivity
            Set<String> newChildPersons = newPersonSet();
            Set<String> newChildGroups = new TreeSet<String>();

            for (String child : group.getChildAssociations()) {
                if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                    newChildPersons.add(child);
                } else {
                    newChildGroups.add(child);
                }
            }

            // Account for differences if already existing
            if (existed) {
                // Update the display name now
                TenantChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName,
                        groupDisplayName);

                // Work out the association differences
                for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
                    if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                        if (!newChildPersons.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    } else {
                        if (!newChildGroups.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    }
                }
            }
            // Mark as created if new
            else {
                // Make sure each group to be created features in the
                // association deletion map (as these are handled in the
                // same phase)
                recordParentAssociationDeletion(groupName, null);
                this.groupsToCreate.put(groupName, groupDisplayName);
            }

            // Create new associations
            for (String child : newChildPersons) {
                // Make sure each person with association changes features
                // as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
            for (String child : newChildGroups) {
                // Make sure each group with association changes features as
                // a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
        }

        private void recordParentAssociationDeletion(String child, String parent) {
            Map<String, Set<String>> parentAssocs;
            if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                parentAssocs = this.personParentAssocsToDelete;
            } else {
                // Reflect the change in the map of final group associations
                // (for cycle detection later)
                parentAssocs = this.groupParentAssocsToDelete;
                if (parent != null) {
                    Set<String> children = this.finalGroupChildAssocs.get(parent);
                    children.remove(child);
                }
            }
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void recordParentAssociationCreation(String child, String parent) {
            Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER
                    ? this.personParentAssocsToCreate
                    : this.groupParentAssocsToCreate;
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void validateGroupParentAssocsToCreate() {
            Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String group = entry.getKey();
                Set<String> parents = entry.getValue();
                Deque<String> visited = new LinkedList<String>();
                Iterator<String> j = parents.iterator();
                while (j.hasNext()) {
                    String parent = j.next();
                    visited.add(parent);
                    if (validateAuthorityChildren(visited, group)) {
                        // The association validated - commit it
                        Set<String> children = finalGroupChildAssocs.get(parent);
                        if (children == null) {
                            children = new TreeSet<String>();
                            finalGroupChildAssocs.put(parent, children);
                        }
                        children.add(group);
                    } else {
                        // The association did not validate - prune it out
                        if (logger.isWarnEnabled()) {
                            TenantChainingUserRegistrySynchronizer.logger.warn("Not adding group '"
                                    + TenantChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(group)
                                    + "' to group '"
                                    + TenantChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent)
                                    + "' as this creates a cyclic relationship");
                        }
                        j.remove();
                    }
                    visited.removeLast();
                }
                if (parents.isEmpty()) {
                    i.remove();
                }
            }

            // Sort the group associations in parent-first order (root
            // groups first) to minimize reindexing overhead
            Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(
                    this.groupParentAssocsToCreate.size() * 2);
            Deque<String> visited = new LinkedList<String>();
            for (String authority : this.groupParentAssocsToCreate.keySet()) {
                visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate,
                        sortedGroupAssociations);
            }

            this.groupParentAssocsToCreate = sortedGroupAssociations;
        }

        private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                return true;
            }
            if (visited.contains(authority)) {
                return false;
            }
            visited.add(authority);
            try {
                Set<String> children = this.finalGroupChildAssocs.get(authority);
                if (children != null) {
                    for (String child : children) {
                        if (!validateAuthorityChildren(visited, child)) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        /**
         * Visits the given authority by recursively visiting its parents in
         * associationsOld and then adding the authority to associationsNew.
         * Used to sort associationsOld into 'parent-first' order to
         * minimize reindexing overhead.
         * 
         * @param visited
         *            The ancestors that form the path to the authority to
         *            visit. Allows detection of cyclic child associations.
         * @param authority
         *            the authority to visit
         * @param associationsOld
         *            the association map to sort
         * @param associationsNew
         *            the association map to add to in parent-first order
         */
        private boolean visitGroupParentAssocs(Deque<String> visited, String authority,
                Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
            if (visited.contains(authority)) {
                // Prevent cyclic paths (Shouldn't happen as we've already
                // validated)
                return false;
            }
            visited.add(authority);
            try {
                if (!associationsNew.containsKey(authority)) {
                    Set<String> oldParents = associationsOld.get(authority);
                    if (oldParents != null) {
                        Set<String> newParents = new TreeSet<String>();

                        for (String parent : oldParents) {
                            if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
                                newParents.add(parent);
                            }
                        }
                        associationsNew.put(authority, newParents);
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        private Set<String> newPersonSet() {
            return TenantChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeSet<String>()
                    : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        }

        private Map<String, Set<String>> newPersonMap() {
            return TenantChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeMap<String, Set<String>>()
                    : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
        }

        private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
            if (toRetain.isEmpty()) {
                parentAssocs.clear();
                return;
            }
            Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
            StringBuilder groupList = null;
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String child = entry.getKey();
                if (!toRetain.contains(child)) {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        if (groupList == null) {
                            groupList = new StringBuilder(1024);
                        } else {
                            groupList.setLength(0);
                        }
                        for (String parent : entry.getValue()) {
                            if (groupList.length() > 0) {
                                groupList.append(", ");
                            }
                            groupList.append('\'')
                                    .append(TenantChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent))
                                    .append('\'');

                        }
                        TenantChainingUserRegistrySynchronizer.logger.debug("Ignoring non-existent member '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(child)
                                + "' in groups {" + groupList.toString() + "}. RunAs user:"
                                + AuthenticationUtil.getRunAsUser());
                    }
                    i.remove();
                }
            }
        }

        public void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
            // If we got back some groups, we have to cross reference them
            // with the set of known authorities
            if (isFullSync || !this.groupParentAssocsToDelete.isEmpty()
                    || !this.groupParentAssocsToDelete.isEmpty()) {
                processGroupSynchInTenantMode(userRegistry, isFullSync, splitTxns);
            }
        }

        private void processGroupSynchInTenantMode(UserRegistry userRegistry, boolean isFullSync,
                boolean splitTxns) {
            final Set<String> allZonePersons = newPersonSet();
            final Set<String> allZoneGroups = new TreeSet<String>();
            final String tenantId = SEIPTenantIntegration.getTenantId();
            // Add in current set of known authorities
            TenantChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                    .doInTransaction(new RetryingTransactionCallback<Void>() {
                        public Void execute() throws Throwable {
                            allZonePersons.addAll(TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
                            allZoneGroups.addAll(TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
                            return null;
                        }
                    }, true, splitTxns);

            allZoneGroups.addAll(this.groupsToCreate.keySet());

            // Prune our set of authorities according to deletions
            if (isFullSync) {
                final Set<String> personDeletionCandidates = newPersonSet();
                personDeletionCandidates.addAll(allZonePersons);

                final Set<String> groupDeletionCandidates = new TreeSet<String>();
                groupDeletionCandidates.addAll(allZoneGroups);

                this.deletionCandidates = new TreeSet<String>();

                for (String person : userRegistry.getPersonNames()) {
                    personDeletionCandidates.remove(person);
                }

                for (String group : userRegistry.getGroupNames()) {
                    groupDeletionCandidates.remove(group);
                }

                this.deletionCandidates = new TreeSet<String>();
                this.deletionCandidates.addAll(personDeletionCandidates);
                this.deletionCandidates.addAll(groupDeletionCandidates);
                if (allowDeletions) {
                    allZonePersons.removeAll(personDeletionCandidates);
                    allZoneGroups.removeAll(groupDeletionCandidates);
                } else {
                    if (!personDeletionCandidates.isEmpty()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn(
                                "The following missing users are not being deleted as allowDeletions == false");
                        for (String person : personDeletionCandidates) {
                            TenantChainingUserRegistrySynchronizer.logger.warn("    " + person);
                        }
                    }
                    if (!groupDeletionCandidates.isEmpty()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn(
                                "The following missing groups are not being deleted as allowDeletions == false");
                        for (String group : groupDeletionCandidates) {
                            TenantChainingUserRegistrySynchronizer.logger.warn("    " + group);
                        }
                    }

                    // Complete association deletion information by
                    // scanning deleted groups
                    BatchProcessor<String> groupScanner = new BatchProcessor<String>(
                            zone + " Missing Authority Scanning",
                            TenantChainingUserRegistrySynchronizer.this.transactionService
                                    .getRetryingTransactionHelper(),
                            this.deletionCandidates, TenantChainingUserRegistrySynchronizer.this.workerThreads,
                            20, TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                            TenantChainingUserRegistrySynchronizer.logger,
                            TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                    groupScanner.process(new BaseBatchProcessWorker<String>() {

                        @Override
                        public String getIdentifier(String entry) {
                            return entry;
                        }

                        @Override
                        public void process(final String authority) throws Throwable {

                            AuthenticationUtil.runAs(new RunAsWork<Void>() {

                                @Override
                                public Void doWork() throws Exception {
                                    proceesInTenantMode(zoneId, authority);
                                    return null;
                                }
                            }, SEIPTenantIntegration.getSystemUserByTenantId(tenantId));

                        }

                        private void proceesInTenantMode(final String zoneId, String authority) {
                            // Disassociate it from this zone, allowing
                            // it to be reclaimed by something further
                            // down the chain
                            TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .removeAuthorityFromZones(authority, Collections.singleton(zoneId));

                            // For groups, remove all members
                            if (AuthorityType.getAuthorityType(authority) != AuthorityType.USER) {
                                String groupShortName = TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authority);
                                String groupDisplayName = TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getAuthorityDisplayName(authority);
                                NodeDescription dummy = new NodeDescription(groupShortName + " (Deleted)");
                                PropertyMap dummyProperties = dummy.getProperties();
                                dummyProperties.put(ContentModel.PROP_AUTHORITY_NAME, authority);
                                if (groupDisplayName != null) {
                                    dummyProperties.put(ContentModel.PROP_AUTHORITY_DISPLAY_NAME,
                                            groupDisplayName);
                                }
                                updateGroup(dummy, true);
                            }
                        }
                    }, splitTxns);

                }
            }

            // Prune the group associations now that we have complete
            // information
            this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
            logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
            this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);

            // Pruning person associations will have to wait until we
            // have passed over all persons and built up
            // this set
            this.allZonePersons = allZonePersons;

            if (!this.groupParentAssocsToDelete.isEmpty()) {
                // Create/update the groups and delete parent
                // associations to be deleted
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        zone + " Group Creation and Association Deletion",
                        TenantChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.groupParentAssocsToDelete.entrySet(),
                        TenantChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        TenantChainingUserRegistrySynchronizer.logger,
                        TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(final Map.Entry<String, Set<String>> entry) throws Throwable {

                        AuthenticationUtil.runAs(new RunAsWork<Void>() {

                            @Override
                            public Void doWork() throws Exception {
                                processInternal(zoneSet, entry.getKey());
                                return null;
                            }
                        }, SEIPTenantIntegration.getSystemUserByTenantId(tenantId));

                    }

                    private void processInternal(final Set<String> zoneSet, String child) {
                        String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
                        if (groupDisplayName != null) {
                            String groupShortName = TenantChainingUserRegistrySynchronizer.this.authorityService
                                    .getShortName(child);
                            if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                                TenantChainingUserRegistrySynchronizer.logger
                                        .debug("Creating group '" + groupShortName + "'");
                            }
                            // create the group
                            TenantChainingUserRegistrySynchronizer.this.authorityService.createAuthority(
                                    AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName,
                                    zoneSet);
                        } else {
                            // Maintain association deletions now. The
                            // creations will have to be done later once
                            // we have performed all the deletions in
                            // order to avoid creating cycles
                            maintainAssociationDeletions(child);
                        }
                    }
                }, splitTxns);
            }
        }

        public void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
            // First validate the group associations to be created for
            // potential cycles. Remove any offending association
            validateGroupParentAssocsToCreate();

            // Now go ahead and create the group associations
            if (!this.groupParentAssocsToCreate.isEmpty()) {
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        zone + " Group Association Creation",
                        TenantChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.groupParentAssocsToCreate.entrySet(),
                        TenantChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        TenantChainingUserRegistrySynchronizer.logger,
                        TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {

                        final String user = entry.getKey();
                        AuthenticationUtil.runAs(new RunAsWork<Void>() {

                            @Override
                            public Void doWork() throws Exception {
                                maintainAssociationCreations(user);
                                return null;
                            }
                        }, SEIPTenantIntegration.getSystemUser(user));
                    }
                }, splitTxns);
            }

            // Remove all the associations we have already dealt with
            this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);

            // Filter out associations to authorities that simply can't
            // exist (and log if debugging is enabled)
            logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);

            // Update associations to persons not updated themselves
            if (!this.personParentAssocsToDelete.isEmpty()) {
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        zone + " Person Association",
                        TenantChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.personParentAssocsToDelete.entrySet(),
                        TenantChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        TenantChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        TenantChainingUserRegistrySynchronizer.logger,
                        TenantChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(final Map.Entry<String, Set<String>> entry) throws Throwable {
                        final String user = entry.getKey();
                        AuthenticationUtil.runAs(new RunAsWork<Void>() {

                            @Override
                            public Void doWork() throws Exception {
                                maintainAssociationDeletions(user);
                                maintainAssociationCreations(user);
                                return null;
                            }
                        }, SEIPTenantIntegration.getSystemUser(user));

                    }
                }, splitTxns);
            }
        }

        private void maintainAssociationDeletions(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName)
                    : this.groupParentAssocsToDelete.get(authorityName);
            if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
                for (String parent : parentsToDelete) {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.debug("Removing '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' from group '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(parent)
                                + "'");
                    }
                    TenantChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent,
                            authorityName);
                }
            }

        }

        private void maintainAssociationCreations(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName)
                    : this.groupParentAssocsToCreate.get(authorityName);
            if (parents != null && !parents.isEmpty()) {
                if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    for (String groupName : parents) {
                        TenantChainingUserRegistrySynchronizer.logger.debug("Adding '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' to group '" + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(groupName)
                                + "'");
                    }
                }
                try {
                    TenantChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents,
                            authorityName);
                } catch (UnknownAuthorityException e) {
                    // Let's force a transaction retry if a parent doesn't
                    // exist. It may be because we are
                    // waiting for another worker thread to create it
                    throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
                } catch (InvalidNodeRefException e) {
                    // Another thread may have written the node, but it is
                    // not visible to this transaction
                    // See: ALF-5471: 'authorityMigration' patch can report
                    // 'Node does not exist'
                    throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
                }
            }
            // Remember that this person's associations have been maintained
            if (isPerson) {
                synchronized (this) {
                    this.personsProcessed.add(authorityName);
                }
            }
        }
    }

    final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
    int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);

    groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);

    // Process persons and their parent associations

    lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
    if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            TenantChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all users from user registry '" + zone + "'");
        } else {
            TenantChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }
    final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
            zone + " User Creation and Association", this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher,
            TenantChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
        private long latestTime;

        public PersonWorker(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(final NodeDescription person) throws Throwable {
            // Make a mutable copy of the person properties, since they get
            // written back to by person service
            HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(
                    person.getProperties());
            String personName = (String) personProperties.get(ContentModel.PROP_USERNAME);
            String ou = (String) personProperties.get(ContentModel.PROP_ORGANIZATION);
            if (SEIPTenantIntegration.isValidTenant(ou) && !personName.endsWith(ou)) {
                personName += ("@" + ou);
                person.getProperties().put(ContentModel.PROP_USERNAME, personName);
            }
            final String personFullName = personName;
            logger.debug("Check user: " + personFullName);
            String tenantDomain = SEIPTenantIntegration.getTenantId(personFullName);
            if (tenantDomain != null && !tenantDomain.isEmpty()) {
                if (!isTenantEnabled(tenantDomain)) {
                    logger.debug("Tenant is missing/disabled for user: " + personFullName);
                    return;
                }
                logger.debug("Process user: " + personFullName);
                AuthenticationUtil.runAs(new RunAsWork<Void>() {
                    @Override
                    public Void doWork() throws Exception {
                        processInTenantMode(personFullName, person);
                        return null;
                    }
                }, SEIPTenantIntegration.getSystemUserByTenantId(tenantDomain));
            } else {
                logger.debug("Process user: " + personFullName);
                processInTenantMode(personFullName, person);
            }
        }

        private void processInTenantMode(String personName, NodeDescription person) {
            HashMap<QName, Serializable> personProperties = person.getProperties();
            // Make a mutable copy of the person properties, since they get
            // written back to by person service

            Set<String> zones = TenantChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(personName);
            if (zones == null) {
                // The person did not exist at all
                if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    TenantChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
                }

                TenantChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                        zoneSet);
            } else if (zones.contains(zoneId)) {
                // The person already existed in this zone: update the
                // person
                if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    TenantChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
                }

                TenantChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                        personProperties, false);
            } else {
                // Check whether the user is in any of the authentication
                // chain zones
                Set<String> intersection = new TreeSet<String>(zones);
                intersection.retainAll(allZoneIds);
                // Check whether the user is in any of the higher priority
                // authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);
                if (visited.size() > 0) {
                    // A person that exists in a different zone with higher
                    // precedence - ignore
                    return;
                }

                else if (!allowDeletions || intersection.isEmpty()) {
                    // The person exists, but in a different zone. Either
                    // deletions are disallowed or the zone is
                    // not in the authentication chain. May be due to
                    // upgrade or zone changes. Let's re-zone them
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
                                + "'. This user will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(personName, zones, zoneSet);
                    TenantChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                            personProperties, false);
                } else {
                    // The person existed, but in a zone with lower
                    // precedence
                    if (TenantChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '"
                                + personName
                                + "'. This user was previously created through synchronization with a lower priority user registry.");
                    }
                    TenantChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
                    TenantChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                            zoneSet);
                }
            }

            // Maintain association deletions and creations in one shot
            // (safe to do this with persons as we can't
            // create cycles)
            groupAnalyzer.maintainAssociationDeletions(personName);
            groupAnalyzer.maintainAssociationCreations(personName);

            synchronized (this) {
                // Maintain the last modified date
                Date personLastModified = person.getLastModified();
                if (personLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
                }
            }
        }
    }

    PersonWorker persons = new PersonWorker(lastModifiedMillis);
    int personProcessedCount = personProcessor.process(persons, splitTxns);

    // Process those associations to persons who themselves have not been
    // updated
    groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);

    // Only now that the whole tree has been processed is it safe to persist
    // the last modified dates
    long latestTime = groupAnalyzer.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }
    latestTime = persons.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(TenantChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }

    // Delete authorities if we have complete information for the zone
    Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
    if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
        BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
                zone + " Authority Deletion", this.transactionService.getRetryingTransactionHelper(),
                deletionCandidates, this.workerThreads, 10, this.applicationEventPublisher,
                TenantChainingUserRegistrySynchronizer.logger, this.loggingInterval);
        class AuthorityDeleter extends BaseBatchProcessWorker<String> {
            private int personProcessedCount;
            private int groupProcessedCount;

            public int getPersonProcessedCount() {
                return this.personProcessedCount;
            }

            public int getGroupProcessedCount() {
                return this.groupProcessedCount;
            }

            public String getIdentifier(String entry) {
                return entry;
            }

            public void process(String authority) throws Throwable {
                if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger
                                .debug("Deleting user '" + authority + "'");
                    }
                    TenantChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
                    synchronized (this) {
                        this.personProcessedCount++;
                    }
                } else {
                    if (TenantChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        TenantChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
                                + TenantChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authority)
                                + "'");
                    }
                    TenantChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
                    synchronized (this) {
                        this.groupProcessedCount++;
                    }
                }
            }
        }
        AuthorityDeleter authorityDeleter = new AuthorityDeleter();
        authorityDeletionProcessor.process(authorityDeleter, splitTxns);
        groupProcessedCount += authorityDeleter.getGroupProcessedCount();
        personProcessedCount += authorityDeleter.getPersonProcessedCount();
    }

    // Remember we have visited this zone
    visitedZoneIds.add(zoneId);

    if (TenantChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        TenantChainingUserRegistrySynchronizer.logger
                .info("Finished synchronizing users and groups with user registry '" + zone + "'");
        TenantChainingUserRegistrySynchronizer.logger
                .info(personProcessedCount + " user(s) and " + groupProcessedCount + " group(s) processed");
    }
}

From source file:org.cggh.repo.security.sync.CustomChainingUserRegistrySynchronizer.java

/**
 * Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling
 * deletions.//w  w  w .j  ava 2s  .co m
 * 
 * @param zone
 *            the zone id. This identifier is used to tag all created groups and users, so that in the future we can
 *            tell those that have been deleted from the registry.
 * @param userRegistry
 *            the user registry for the zone.
 * @param forceUpdate
 *            Should the complete set of users and groups be updated / created locally or just those known to have
 *            changed since the last sync? When <code>true</code> then <i>all</i> users and groups are queried from
 *            the user registry and updated locally. When <code>false</code> then each source is only queried for
 *            those users and groups modified since the most recent modification date of all the objects last
 *            queried from that same source.
 * @param isFullSync
 *            Should a complete set of user and group IDs be queried from the user registries in order to determine
 *            deletions? This parameter is independent of <code>force</code> as a separate query is run to process
 *            updates.
 * @param splitTxns
 *            Can the modifications to Alfresco be split across multiple transactions for maximum performance? If
 *            <code>true</code>, users and groups are created/updated in batches for increased performance. If
 *            <code>false</code>, all users and groups are processed in the current transaction. This is required if
 *            calling synchronously (e.g. in response to an authentication event in the same transaction).
 * @param visitedZoneIds
 *            the set of zone ids already processed. These zones have precedence over the current zone when it comes
 *            to group name 'collisions'. If a user or group is queried that already exists locally but is tagged
 *            with one of the zones in this set, then it will be ignored as this zone has lower priority.
 * @param allZoneIds
 *            the set of all zone ids in the authentication chain. Helps us work out whether the zone information
 *            recorded against a user or group is invalid for the current authentication chain and whether the user
 *            or group needs to be 're-zoned'.
 */
private void syncWithPlugin(final String zone, UserRegistry userRegistry, boolean forceUpdate,
        boolean isFullSync, boolean splitTxns, final Set<String> visitedZoneIds, final Set<String> allZoneIds) {
    // Create a prefixed zone ID for use with the authority service
    final String zoneId = AuthorityService.ZONE_AUTH_EXT_PREFIX + zone;

    // Batch Process Names
    final String reservedBatchProcessNames[] = { SyncProcess.GROUP_ANALYSIS.getTitle(zone),
            SyncProcess.USER_CREATION.getTitle(zone), SyncProcess.MISSING_AUTHORITY.getTitle(zone),
            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
            SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
            SyncProcess.PERSON_ASSOCIATION.getTitle(zone), SyncProcess.AUTHORITY_DELETION.getTitle(zone) };

    notifySyncDirectoryStart(zone, reservedBatchProcessNames);

    // Ensure that the zoneId exists before multiple threads start using it
    this.transactionService.getRetryingTransactionHelper()
            .doInTransaction(new RetryingTransactionCallback<Void>() {
                @Override
                public Void execute() throws Throwable {
                    authorityService.getOrCreateZone(zoneId);
                    return null;
                }
            }, false, splitTxns);

    // The set of zones we associate with new objects (default plus registry specific)
    final Set<String> zoneSet = getZones(zoneId);

    long lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    Date lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);

    if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            CustomChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all groups from user registry '" + zone + "'");
        } else {
            CustomChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving groups changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // First, analyze the group structure. Create maps of authorities to their parents for associations to create
    // and delete. Also deal with 'overlaps' with other zones in the authentication chain.
    final BatchProcessor<NodeDescription> groupProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.GROUP_ANALYSIS.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getGroups(lastModified), this.workerThreads, 20, this.applicationEventPublisher,
            CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class Analyzer extends BaseBatchProcessWorker<NodeDescription> {
        private final Map<String, String> groupsToCreate = new TreeMap<String, String>();
        private final Map<String, Set<String>> personParentAssocsToCreate = newPersonMap();
        private final Map<String, Set<String>> personParentAssocsToDelete = newPersonMap();
        private Map<String, Set<String>> groupParentAssocsToCreate = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> groupParentAssocsToDelete = new TreeMap<String, Set<String>>();
        private final Map<String, Set<String>> finalGroupChildAssocs = new TreeMap<String, Set<String>>();
        private List<String> personsProcessed = new LinkedList<String>();
        private Set<String> allZonePersons = Collections.emptySet();
        private Set<String> deletionCandidates;

        private long latestTime;

        public Analyzer(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public Set<String> getDeletionCandidates() {
            return this.deletionCandidates;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription group) throws Throwable {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupShortName = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getShortName(groupName);
            Set<String> groupZones = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(groupName);

            if (groupZones == null) {
                // The group did not exist at all
                updateGroup(group, false);
            } else {
                // Check whether the group is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(groupZones);
                intersection.retainAll(allZoneIds);
                // Check whether the group is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);

                if (groupZones.contains(zoneId)) {
                    // The group already existed in this zone: update the group
                    updateGroup(group, true);
                } else if (!visited.isEmpty()) {
                    // A group that exists in a different zone with higher precedence
                    return;
                } else if (!allowDeletions || intersection.isEmpty()) {
                    // Deletions are disallowed or the group exists, but not in a zone that's in the authentication
                    // chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Updating group '" + groupShortName
                                + "'. This group will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(groupName, groupZones, zoneSet);

                    // The group now exists in this zone: update the group
                    updateGroup(group, true);
                } else {
                    // The group existed, but in a zone with lower precedence
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Recreating occluded group '"
                                + groupShortName
                                + "'. This group was previously created through synchronization with a lower priority user registry.");
                    }
                    CustomChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(groupName);

                    // create the group
                    updateGroup(group, false);
                }
            }

            synchronized (this) {
                // Maintain the last modified date
                Date groupLastModified = group.getLastModified();
                if (groupLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, groupLastModified.getTime());
                }
            }
        }

        // Recursively walks and caches the authorities relating to and from this group so that we can later detect potential cycles
        private Set<String> getContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // First, recurse to the parent most authorities
            for (String parent : CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getContainingAuthorities(null, groupName, true)) {
                getContainedAuthorities(parent);
            }

            // Now descend on unprocessed parents.
            return cacheContainedAuthorities(groupName);
        }

        private Set<String> cacheContainedAuthorities(String groupName) {
            // Return the cached children if it is processed
            Set<String> children = this.finalGroupChildAssocs.get(groupName);
            if (children != null) {
                return children;
            }

            // Descend on unprocessed parents.
            children = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getContainedAuthorities(null, groupName, true);
            this.finalGroupChildAssocs.put(groupName, children);

            for (String child : children) {
                if (AuthorityType.getAuthorityType(child) != AuthorityType.USER) {
                    cacheContainedAuthorities(child);
                }
            }
            return children;
        }

        private synchronized void updateGroup(NodeDescription group, boolean existed) {
            PropertyMap groupProperties = group.getProperties();
            String groupName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_NAME);
            String groupDisplayName = (String) groupProperties.get(ContentModel.PROP_AUTHORITY_DISPLAY_NAME);
            if (groupDisplayName == null) {
                groupDisplayName = CustomChainingUserRegistrySynchronizer.this.authorityService
                        .getShortName(groupName);
            }

            // Divide the child associations into person and group associations, dealing with case sensitivity
            Set<String> newChildPersons = newPersonSet();
            Set<String> newChildGroups = new TreeSet<String>();

            for (String child : group.getChildAssociations()) {
                if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                    newChildPersons.add(child);
                } else {
                    newChildGroups.add(child);
                }
            }

            // Account for differences if already existing
            if (existed) {
                // Update the display name now
                CustomChainingUserRegistrySynchronizer.this.authorityService.setAuthorityDisplayName(groupName,
                        groupDisplayName);

                // Work out the association differences
                for (String child : new TreeSet<String>(getContainedAuthorities(groupName))) {
                    if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                        if (!newChildPersons.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    } else {
                        if (!newChildGroups.remove(child)) {
                            recordParentAssociationDeletion(child, groupName);
                        }
                    }
                }
            }
            // Mark as created if new
            else {
                // Make sure each group to be created features in the association deletion map (as these are handled in the same phase)
                recordParentAssociationDeletion(groupName, null);
                this.groupsToCreate.put(groupName, groupDisplayName);
            }

            // Create new associations
            for (String child : newChildPersons) {
                // Make sure each person with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
            for (String child : newChildGroups) {
                // Make sure each group with association changes features as a key in the deletion map
                recordParentAssociationDeletion(child, null);
                recordParentAssociationCreation(child, groupName);
            }
        }

        private void recordParentAssociationDeletion(String child, String parent) {
            Map<String, Set<String>> parentAssocs;
            if (AuthorityType.getAuthorityType(child) == AuthorityType.USER) {
                parentAssocs = this.personParentAssocsToDelete;
            } else {
                // Reflect the change in the map of final group associations (for cycle detection later)
                parentAssocs = this.groupParentAssocsToDelete;
                if (parent != null) {
                    Set<String> children = this.finalGroupChildAssocs.get(parent);
                    children.remove(child);
                }
            }
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void recordParentAssociationCreation(String child, String parent) {
            Map<String, Set<String>> parentAssocs = AuthorityType.getAuthorityType(child) == AuthorityType.USER
                    ? this.personParentAssocsToCreate
                    : this.groupParentAssocsToCreate;
            Set<String> parents = parentAssocs.get(child);
            if (parents == null) {
                parents = new TreeSet<String>();
                parentAssocs.put(child, parents);
            }
            if (parent != null) {
                parents.add(parent);
            }
        }

        private void validateGroupParentAssocsToCreate() {
            Iterator<Map.Entry<String, Set<String>>> i = this.groupParentAssocsToCreate.entrySet().iterator();
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String group = entry.getKey();
                Set<String> parents = entry.getValue();
                Deque<String> visited = new LinkedList<String>();
                Iterator<String> j = parents.iterator();
                while (j.hasNext()) {
                    String parent = j.next();
                    visited.add(parent);
                    if (validateAuthorityChildren(visited, group)) {
                        // The association validated - commit it
                        Set<String> children = finalGroupChildAssocs.get(parent);
                        if (children == null) {
                            children = new TreeSet<String>();
                            finalGroupChildAssocs.put(parent, children);
                        }
                        children.add(group);
                    } else {
                        // The association did not validate - prune it out
                        if (logger.isWarnEnabled()) {
                            CustomChainingUserRegistrySynchronizer.logger.warn("Not adding group '"
                                    + CustomChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(group)
                                    + "' to group '"
                                    + CustomChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent)
                                    + "' as this creates a cyclic relationship");
                        }
                        j.remove();
                    }
                    visited.removeLast();
                }
                if (parents.isEmpty()) {
                    i.remove();
                }
            }

            // Sort the group associations in parent-first order (root groups first) to minimize reindexing overhead
            Map<String, Set<String>> sortedGroupAssociations = new LinkedHashMap<String, Set<String>>(
                    this.groupParentAssocsToCreate.size() * 2);
            Deque<String> visited = new LinkedList<String>();
            for (String authority : this.groupParentAssocsToCreate.keySet()) {
                visitGroupParentAssocs(visited, authority, this.groupParentAssocsToCreate,
                        sortedGroupAssociations);
            }

            this.groupParentAssocsToCreate = sortedGroupAssociations;
        }

        private boolean validateAuthorityChildren(Deque<String> visited, String authority) {
            if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                return true;
            }
            if (visited.contains(authority)) {
                return false;
            }
            visited.add(authority);
            try {
                Set<String> children = this.finalGroupChildAssocs.get(authority);
                if (children != null) {
                    for (String child : children) {
                        if (!validateAuthorityChildren(visited, child)) {
                            return false;
                        }
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        /**
         * Visits the given authority by recursively visiting its parents in associationsOld and then adding the
         * authority to associationsNew. Used to sort associationsOld into 'parent-first' order to minimize
         * reindexing overhead.
         * 
         * @param visited
         *            The ancestors that form the path to the authority to visit. Allows detection of cyclic child
         *            associations.
         * @param authority
         *            the authority to visit
         * @param associationsOld
         *            the association map to sort
         * @param associationsNew
         *            the association map to add to in parent-first order
         */
        private boolean visitGroupParentAssocs(Deque<String> visited, String authority,
                Map<String, Set<String>> associationsOld, Map<String, Set<String>> associationsNew) {
            if (visited.contains(authority)) {
                // Prevent cyclic paths (Shouldn't happen as we've already validated)
                return false;
            }
            visited.add(authority);
            try {
                if (!associationsNew.containsKey(authority)) {
                    Set<String> oldParents = associationsOld.get(authority);
                    if (oldParents != null) {
                        Set<String> newParents = new TreeSet<String>();

                        for (String parent : oldParents) {
                            if (visitGroupParentAssocs(visited, parent, associationsOld, associationsNew)) {
                                newParents.add(parent);
                            }
                        }
                        associationsNew.put(authority, newParents);
                    }
                }
                return true;
            } finally {
                visited.removeLast();
            }
        }

        private Set<String> newPersonSet() {
            return CustomChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeSet<String>()
                    : new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
        }

        private Map<String, Set<String>> newPersonMap() {
            return CustomChainingUserRegistrySynchronizer.this.personService.getUserNamesAreCaseSensitive()
                    ? new TreeMap<String, Set<String>>()
                    : new TreeMap<String, Set<String>>(String.CASE_INSENSITIVE_ORDER);
        }

        private void logRetainParentAssociations(Map<String, Set<String>> parentAssocs, Set<String> toRetain) {
            Iterator<Map.Entry<String, Set<String>>> i = parentAssocs.entrySet().iterator();
            StringBuilder groupList = null;
            while (i.hasNext()) {
                Map.Entry<String, Set<String>> entry = i.next();
                String child = entry.getKey();
                if (!toRetain.contains(child)) {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        if (groupList == null) {
                            groupList = new StringBuilder(1024);
                        } else {
                            groupList.setLength(0);
                        }
                        for (String parent : entry.getValue()) {
                            if (groupList.length() > 0) {
                                groupList.append(", ");
                            }
                            groupList.append('\'')
                                    .append(CustomChainingUserRegistrySynchronizer.this.authorityService
                                            .getShortName(parent))
                                    .append('\'');

                        }
                        CustomChainingUserRegistrySynchronizer.logger
                                .debug("Ignoring non-existent member '"
                                        + CustomChainingUserRegistrySynchronizer.this.authorityService
                                                .getShortName(child)
                                        + "' in groups {" + groupList.toString() + "}");
                    }
                    i.remove();
                }
            }
        }

        private void processGroups(UserRegistry userRegistry, boolean isFullSync, boolean splitTxns) {
            // MNT-12454 fix. If syncDelete is false, there is no need to pull all users and all groups from LDAP during the full synchronization.
            if ((syncDelete || !groupsToCreate.isEmpty())
                    && (isFullSync || !this.groupParentAssocsToDelete.isEmpty())) {
                final Set<String> allZonePersons = newPersonSet();
                final Set<String> allZoneGroups = new TreeSet<String>();

                // Add in current set of known authorities
                CustomChainingUserRegistrySynchronizer.this.transactionService.getRetryingTransactionHelper()
                        .doInTransaction(new RetryingTransactionCallback<Void>() {
                            public Void execute() throws Throwable {
                                allZonePersons
                                        .addAll(CustomChainingUserRegistrySynchronizer.this.authorityService
                                                .getAllAuthoritiesInZone(zoneId, AuthorityType.USER));
                                allZoneGroups
                                        .addAll(CustomChainingUserRegistrySynchronizer.this.authorityService
                                                .getAllAuthoritiesInZone(zoneId, AuthorityType.GROUP));
                                return null;
                            }
                        }, true, splitTxns);

                allZoneGroups.addAll(this.groupsToCreate.keySet());

                // Prune our set of authorities according to deletions
                if (isFullSync) {
                    final Set<String> personDeletionCandidates = newPersonSet();
                    personDeletionCandidates.addAll(allZonePersons);

                    final Set<String> groupDeletionCandidates = new TreeSet<String>();
                    groupDeletionCandidates.addAll(allZoneGroups);

                    this.deletionCandidates = new TreeSet<String>();

                    for (String person : userRegistry.getPersonNames()) {
                        personDeletionCandidates.remove(person);
                    }

                    for (String group : userRegistry.getGroupNames()) {
                        groupDeletionCandidates.remove(group);
                    }

                    this.deletionCandidates = new TreeSet<String>();
                    this.deletionCandidates.addAll(personDeletionCandidates);
                    this.deletionCandidates.addAll(groupDeletionCandidates);

                    if (allowDeletions) {
                        allZonePersons.removeAll(personDeletionCandidates);
                        allZoneGroups.removeAll(groupDeletionCandidates);
                    } else {
                        // Complete association deletion information by scanning deleted groups
                        BatchProcessor<String> groupScanner = new BatchProcessor<String>(
                                zone + " Missing Authority Scanning",
                                CustomChainingUserRegistrySynchronizer.this.transactionService
                                        .getRetryingTransactionHelper(),
                                this.deletionCandidates,
                                CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                                CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                                CustomChainingUserRegistrySynchronizer.logger,
                                CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                        groupScanner.process(new BaseBatchProcessWorker<String>() {

                            @Override
                            public String getIdentifier(String entry) {
                                return entry;
                            }

                            @Override
                            public void process(String authority) throws Throwable {
                                //MNT-12454 fix. Modifies an authority's zone. Move authority from AUTH.EXT.LDAP1 to AUTH.ALF.
                                updateAuthorityZones(authority, Collections.singleton(zoneId),
                                        Collections.singleton(AuthorityService.ZONE_AUTH_ALFRESCO));
                            }
                        }, splitTxns);
                    }

                }

                // Prune the group associations now that we have complete information
                this.groupParentAssocsToCreate.keySet().retainAll(allZoneGroups);
                logRetainParentAssociations(this.groupParentAssocsToCreate, allZoneGroups);
                this.finalGroupChildAssocs.keySet().retainAll(allZoneGroups);

                // Pruning person associations will have to wait until we have passed over all persons and built up
                // this set
                this.allZonePersons = allZonePersons;

                if (!this.groupParentAssocsToDelete.isEmpty()) {
                    // Create/update the groups and delete parent associations to be deleted
                    // Batch 4 Group Creation and Association Deletion
                    BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                            SyncProcess.GROUP_CREATION_AND_ASSOCIATION_DELETION.getTitle(zone),
                            CustomChainingUserRegistrySynchronizer.this.transactionService
                                    .getRetryingTransactionHelper(),
                            this.groupParentAssocsToDelete.entrySet(),
                            CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                            CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                            CustomChainingUserRegistrySynchronizer.logger,
                            CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                    groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                        public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                            return entry.getKey() + " " + entry.getValue();
                        }

                        public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                            String child = entry.getKey();

                            String groupDisplayName = Analyzer.this.groupsToCreate.get(child);
                            if (groupDisplayName != null) {
                                String groupShortName = CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(child);
                                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                                    CustomChainingUserRegistrySynchronizer.logger
                                            .debug("Creating group '" + groupShortName + "'");
                                }
                                // create the group
                                CustomChainingUserRegistrySynchronizer.this.authorityService.createAuthority(
                                        AuthorityType.getAuthorityType(child), groupShortName, groupDisplayName,
                                        zoneSet);
                            } else {
                                // Maintain association deletions now. The creations will have to be done later once
                                // we have performed all the deletions in order to avoid creating cycles
                                maintainAssociationDeletions(child);
                            }
                        }
                    }, splitTxns);
                }
            }
        }

        private void finalizeAssociations(UserRegistry userRegistry, boolean splitTxns) {
            // First validate the group associations to be created for potential cycles. Remove any offending association
            validateGroupParentAssocsToCreate();

            // Now go ahead and create the group associations
            if (!this.groupParentAssocsToCreate.isEmpty()) {
                // Batch 5 Group Association Creation
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.GROUP_ASSOCIATION_CREATION.getTitle(zone),
                        CustomChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.groupParentAssocsToCreate.entrySet(),
                        CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        CustomChainingUserRegistrySynchronizer.logger,
                        CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }

            // Remove all the associations we have already dealt with
            this.personParentAssocsToDelete.keySet().removeAll(this.personsProcessed);

            // Filter out associations to authorities that simply can't exist (and log if debugging is enabled)
            logRetainParentAssociations(this.personParentAssocsToCreate, this.allZonePersons);

            // Update associations to persons not updated themselves
            if (!this.personParentAssocsToDelete.isEmpty()) {
                // Batch 6 Person Association
                BatchProcessor<Map.Entry<String, Set<String>>> groupCreator = new BatchProcessor<Map.Entry<String, Set<String>>>(
                        SyncProcess.PERSON_ASSOCIATION.getTitle(zone),
                        CustomChainingUserRegistrySynchronizer.this.transactionService
                                .getRetryingTransactionHelper(),
                        this.personParentAssocsToDelete.entrySet(),
                        CustomChainingUserRegistrySynchronizer.this.workerThreads, 20,
                        CustomChainingUserRegistrySynchronizer.this.applicationEventPublisher,
                        CustomChainingUserRegistrySynchronizer.logger,
                        CustomChainingUserRegistrySynchronizer.this.loggingInterval);
                groupCreator.process(new BaseBatchProcessWorker<Map.Entry<String, Set<String>>>() {
                    public String getIdentifier(Map.Entry<String, Set<String>> entry) {
                        return entry.getKey() + " " + entry.getValue();
                    }

                    public void process(Map.Entry<String, Set<String>> entry) throws Throwable {
                        maintainAssociationDeletions(entry.getKey());
                        maintainAssociationCreations(entry.getKey());
                    }
                }, splitTxns);
            }
        }

        private void maintainAssociationDeletions(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parentsToDelete = isPerson ? this.personParentAssocsToDelete.get(authorityName)
                    : this.groupParentAssocsToDelete.get(authorityName);
            if (parentsToDelete != null && !parentsToDelete.isEmpty()) {
                for (String parent : parentsToDelete) {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.debug("Removing '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' from group '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(parent)
                                + "'");
                    }
                    CustomChainingUserRegistrySynchronizer.this.authorityService.removeAuthority(parent,
                            authorityName);
                }
            }

        }

        private void maintainAssociationCreations(String authorityName) {
            boolean isPerson = AuthorityType.getAuthorityType(authorityName) == AuthorityType.USER;
            Set<String> parents = isPerson ? this.personParentAssocsToCreate.get(authorityName)
                    : this.groupParentAssocsToCreate.get(authorityName);
            if (parents != null && !parents.isEmpty()) {
                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    for (String groupName : parents) {
                        CustomChainingUserRegistrySynchronizer.logger.debug("Adding '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authorityName)
                                + "' to group '" + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(groupName)
                                + "'");
                    }
                }
                try {
                    CustomChainingUserRegistrySynchronizer.this.authorityService.addAuthority(parents,
                            authorityName);
                } catch (UnknownAuthorityException e) {
                    // Let's force a transaction retry if a parent doesn't exist. It may be because we are
                    // waiting for another worker thread to create it
                    throw new ConcurrencyFailureException("Forcing batch retry for unknown authority", e);
                } catch (InvalidNodeRefException e) {
                    // Another thread may have written the node, but it is not visible to this transaction
                    // See: ALF-5471: 'authorityMigration' patch can report 'Node does not exist'
                    throw new ConcurrencyFailureException("Forcing batch retry for invalid node", e);
                }
            }
            // Remember that this person's associations have been maintained
            if (isPerson) {
                synchronized (this) {
                    this.personsProcessed.add(authorityName);
                }
            }
        }
    } // end of Analyzer class

    // Run the first process the Group Analyzer
    final Analyzer groupAnalyzer = new Analyzer(lastModifiedMillis);
    int groupProcessedCount = groupProcessor.process(groupAnalyzer, splitTxns);

    groupAnalyzer.processGroups(userRegistry, isFullSync, splitTxns);

    // Process persons and their parent associations

    lastModifiedMillis = forceUpdate ? -1
            : getMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE,
                    zoneId, splitTxns);
    lastModified = lastModifiedMillis == -1 ? null : new Date(lastModifiedMillis);
    if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        if (lastModified == null) {
            CustomChainingUserRegistrySynchronizer.logger
                    .info("Retrieving all users from user registry '" + zone + "'");
        } else {
            CustomChainingUserRegistrySynchronizer.logger.info(
                    "Retrieving users changed since " + DateFormat.getDateTimeInstance().format(lastModified)
                            + " from user registry '" + zone + "'");
        }
    }

    // User Creation and Association
    final BatchProcessor<NodeDescription> personProcessor = new BatchProcessor<NodeDescription>(
            SyncProcess.USER_CREATION.getTitle(zone), this.transactionService.getRetryingTransactionHelper(),
            userRegistry.getPersons(lastModified), this.workerThreads, 10, this.applicationEventPublisher,
            CustomChainingUserRegistrySynchronizer.logger, this.loggingInterval);
    class PersonWorker extends BaseBatchProcessWorker<NodeDescription> {
        private long latestTime;

        public PersonWorker(final long latestTime) {
            this.latestTime = latestTime;
        }

        public long getLatestTime() {
            return this.latestTime;
        }

        public String getIdentifier(NodeDescription entry) {
            return entry.getSourceId();
        }

        public void process(NodeDescription person) throws Throwable {
            // Make a mutable copy of the person properties, since they get written back to by person service
            HashMap<QName, Serializable> personProperties = new HashMap<QName, Serializable>(
                    person.getProperties());
            String personName = personProperties.get(ContentModel.PROP_USERNAME).toString().trim();
            personProperties.put(ContentModel.PROP_USERNAME, personName);
            // for invalid names will throw ConstraintException that will be catched by BatchProcessor$TxnCallback
            nameChecker.evaluate(personName);
            Set<String> zones = CustomChainingUserRegistrySynchronizer.this.authorityService
                    .getAuthorityZones(personName);
            if (zones == null) {
                // The person did not exist at all
                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    CustomChainingUserRegistrySynchronizer.logger.debug("Creating user '" + personName + "'");
                }
                CustomChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                        zoneSet);
                CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                        person.getProperties(), getLatestTime());
            } else if (zones.contains(zoneId)) {
                // The person already existed in this zone: update the person
                if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                    CustomChainingUserRegistrySynchronizer.logger.debug("Updating user '" + personName + "'");
                }
                CustomChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                        personProperties, false);
                CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                        person.getProperties(), getLatestTime());
            } else {
                // Check whether the user is in any of the authentication chain zones
                Set<String> intersection = new TreeSet<String>(zones);
                intersection.retainAll(allZoneIds);
                // Check whether the user is in any of the higher priority authentication chain zones
                Set<String> visited = new TreeSet<String>(intersection);
                visited.retainAll(visitedZoneIds);
                if (visited.size() > 0) {
                    // A person that exists in a different zone with higher precedence - ignore
                    return;
                }

                else if (!allowDeletions || intersection.isEmpty()) {
                    // The person exists, but in a different zone. Either deletions are disallowed or the zone is
                    // not in the authentication chain. May be due to upgrade or zone changes. Let's re-zone them
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Updating user '" + personName
                                + "'. This user will in future be assumed to originate from user registry '"
                                + zone + "'.");
                    }
                    updateAuthorityZones(personName, zones, zoneSet);
                    CustomChainingUserRegistrySynchronizer.this.personService.setPersonProperties(personName,
                            personProperties, false);
                    CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                            person.getProperties(), getLatestTime());
                } else {
                    // The person existed, but in a zone with lower precedence
                    if (CustomChainingUserRegistrySynchronizer.logger.isWarnEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.warn("Recreating occluded user '"
                                + personName
                                + "'. This user was previously created through synchronization with a lower priority user registry.");
                    }
                    CustomChainingUserRegistrySynchronizer.this.personService.deletePerson(personName);
                    CustomChainingUserRegistrySynchronizer.this.personService.createPerson(personProperties,
                            zoneSet);
                    CustomChainingUserRegistrySynchronizer.this.avatarService.setAvatar(personName,
                            person.getProperties(), getLatestTime());
                }
            }

            // Maintain association deletions and creations in one shot (safe to do this with persons as we can't
            // create cycles)
            groupAnalyzer.maintainAssociationDeletions(personName);
            groupAnalyzer.maintainAssociationCreations(personName);

            synchronized (this) {
                // Maintain the last modified date
                Date personLastModified = person.getLastModified();
                if (personLastModified != null) {
                    this.latestTime = Math.max(this.latestTime, personLastModified.getTime());
                }
            }
        }
    }

    PersonWorker persons = new PersonWorker(lastModifiedMillis);
    int personProcessedCount = personProcessor.process(persons, splitTxns);

    // Process those associations to persons who themselves have not been updated
    groupAnalyzer.finalizeAssociations(userRegistry, splitTxns);

    // Only now that the whole tree has been processed is it safe to persist the last modified dates
    long latestTime = groupAnalyzer.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.GROUP_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }
    latestTime = persons.getLatestTime();
    if (latestTime != -1) {
        setMostRecentUpdateTime(CustomChainingUserRegistrySynchronizer.PERSON_LAST_MODIFIED_ATTRIBUTE, zoneId,
                latestTime, splitTxns);
    }

    // Delete authorities if we have complete information for the zone
    Set<String> deletionCandidates = groupAnalyzer.getDeletionCandidates();
    if (isFullSync && allowDeletions && !deletionCandidates.isEmpty()) {
        // Batch 7 Authority Deletion
        BatchProcessor<String> authorityDeletionProcessor = new BatchProcessor<String>(
                SyncProcess.AUTHORITY_DELETION.getTitle(zone),
                this.transactionService.getRetryingTransactionHelper(), deletionCandidates, this.workerThreads,
                10, this.applicationEventPublisher, CustomChainingUserRegistrySynchronizer.logger,
                this.loggingInterval);
        class AuthorityDeleter extends BaseBatchProcessWorker<String> {
            private int personProcessedCount;
            private int groupProcessedCount;

            public int getPersonProcessedCount() {
                return this.personProcessedCount;
            }

            public int getGroupProcessedCount() {
                return this.groupProcessedCount;
            }

            public String getIdentifier(String entry) {
                return entry;
            }

            public void process(String authority) throws Throwable {
                if (AuthorityType.getAuthorityType(authority) == AuthorityType.USER) {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger
                                .debug("Deleting user '" + authority + "'");
                    }
                    CustomChainingUserRegistrySynchronizer.this.personService.deletePerson(authority);
                    synchronized (this) {
                        this.personProcessedCount++;
                    }
                } else {
                    if (CustomChainingUserRegistrySynchronizer.logger.isDebugEnabled()) {
                        CustomChainingUserRegistrySynchronizer.logger.debug("Deleting group '"
                                + CustomChainingUserRegistrySynchronizer.this.authorityService
                                        .getShortName(authority)
                                + "'");
                    }
                    CustomChainingUserRegistrySynchronizer.this.authorityService.deleteAuthority(authority);
                    synchronized (this) {
                        this.groupProcessedCount++;
                    }
                }
            }
        }
        AuthorityDeleter authorityDeleter = new AuthorityDeleter();
        authorityDeletionProcessor.process(authorityDeleter, splitTxns);
        groupProcessedCount += authorityDeleter.getGroupProcessedCount();
        personProcessedCount += authorityDeleter.getPersonProcessedCount();
    }

    // Remember we have visited this zone
    visitedZoneIds.add(zoneId);

    Object statusParams[] = { personProcessedCount, groupProcessedCount };
    final String statusMessage = I18NUtil.getMessage("synchronization.summary.status", statusParams);

    if (CustomChainingUserRegistrySynchronizer.logger.isInfoEnabled()) {
        CustomChainingUserRegistrySynchronizer.logger
                .info("Finished synchronizing users and groups with user registry '" + zone + "'");
        CustomChainingUserRegistrySynchronizer.logger.info(statusMessage);
    }

    notifySyncDirectoryEnd(zone, statusMessage);

}

From source file:org.dataconservancy.packaging.tool.impl.DcsBoPackageOntologyServiceImpl.java

/**
 * Gets the available types for the given artifact, based on it's contextual location in the tree.
 * /*from  w  ww . ja  v a  2s . c  o m*/
 * If called on ignored node, take into account ignored children, otherwise do not take into account ignored children.
 *
 * @param tree The tree representing the {@link org.dataconservancy.packaging.tool.model.PackageDescription}
 * @param currentArtifactId The id of the artifact to retrieve the types for.
 * @return A set of all the available relationships for the artifact.
 */
@Override
public Set<String> getValidTypes(PackageTree tree, String currentArtifactId) throws PackageOntologyException {
    if (ontology == null) {
        throw new RuntimeException("Ontology and hierarchical relationship names all have to be specified, "
                + "before service could be used.");
    }

    if (tree == null || currentArtifactId == null) {
        throw new PackageOntologyException("Package tree and currentArifactId cannot be null");
    }

    //retrieve current Node from nodeMaps
    PackageNode currentNode = tree.getNodesMap().get(currentArtifactId);

    //filter out valid types for artifact's filesystem types (ie. directories cannot be files and bytestreams have
    //to be files
    Set<String> validTypes = getValidTypesPerFileSystemType(currentNode.getValue().isByteStream());

    Set<String> validTypesDictatedByParent;

    validTypesDictatedByParent = new HashSet<>();
    PackageNode parentNode = currentNode.getParentNode();

    //if a parent node of with the same artifactId exists in the NodeMap
    //calculate the valid types based on parent's type
    if (parentNode != null) {
        //if node exists, check for its PackageArtifact's type
        String parentNodeType = parentNode.getValue().getType();
        //look up the source target pairs by relationship name:
        for (String relationshipToParent : DcsBoPackageOntology.relationshipsToParent) {
            Set<RelationshipSourceTargetPair> sourceTargetPairs = relationshipCatalog.get(relationshipToParent);
            if (sourceTargetPairs != null) {
                //For every combination of source-target for the relationship
                for (RelationshipSourceTargetPair pair : sourceTargetPairs) {
                    if (pair.getTargetType().equals(parentNodeType)) {
                        //add up all valid types determined by this relationship to parent
                        validTypesDictatedByParent.add(pair.getSourceType());
                    }
                }
            }
        }
        //By this point, should have a set of types dictated by the parent of the relationship
        //intersect with all types to get an intermediate set of valid types.
        //Only intersect the set if validByParent set is not empty
        if (validTypesDictatedByParent.size() > 0) {
            validTypes.retainAll(validTypesDictatedByParent);
        }
    }

    // If node is ignored, consider all kids.
    // If node is not ignored, only consider kids that are not ignored.

    List<PackageNode> kids;

    if (currentNode.getValue().isIgnored()) {
        kids = currentNode.getChildrenNodes();
    } else {
        kids = getKidsWithIgnoredStatus(currentNode, false);
    }

    if (kids.size() > 0) {
        Set<String> validTypesDictatedByChildren;

        for (PackageNode childNode : kids) {
            //if node exists, check for its PackageArtifact's type
            String childNodeType = childNode.getValue().getType();
            //If child is not a File type
            if (!childNodeType.contains("File")) {
                for (String relationshipToParent : DcsBoPackageOntology.relationshipsToParent) {
                    validTypesDictatedByChildren = new HashSet<>();
                    //look up the source target pairs by relationship name:
                    Set<RelationshipSourceTargetPair> sourceTargetPairs = relationshipCatalog
                            .get(relationshipToParent);
                    if (sourceTargetPairs != null) {
                        //For every combination of source of the relationship
                        for (RelationshipSourceTargetPair pair : sourceTargetPairs) {
                            //get valid relationship's target
                            if (pair.getSourceType().equals(childNodeType)) {
                                validTypesDictatedByChildren.add(pair.getTargetType());
                            }
                        }
                    }
                    //If there are some type restriction by the children of this relationship
                    if (validTypesDictatedByChildren.size() > 0) {
                        //intersect the set of types dictate by children with the main validTypes set.
                        validTypes.retainAll(validTypesDictatedByChildren);
                    }
                }
            } else { // if child is a file Type
                //to deal with the isMetadataFor and isMemberOf being interchangeable in the case of Files.
                validTypesDictatedByChildren = new HashSet<>();
                for (String relationshipToParent : DcsBoPackageOntology.relationshipsToParent) {
                    //look up the source target pairs by relationship name:
                    Set<RelationshipSourceTargetPair> sourceTargetPairs = relationshipCatalog
                            .get(relationshipToParent);
                    if (sourceTargetPairs != null) {
                        for (RelationshipSourceTargetPair pair : sourceTargetPairs) {
                            if (pair.getSourceType().contains("File")) {
                                validTypesDictatedByChildren.add(pair.getTargetType());
                            }
                        }
                    }
                }
                //If there are some type restriction by the children of this relationship
                if (validTypesDictatedByChildren.size() > 0) {
                    //intersect the set of types dictate by children with the main validTypes set.
                    validTypes.retainAll(validTypesDictatedByChildren);
                }
            }
        }
    } else if (canBeDataItemFile(tree, currentArtifactId)) {
        validTypes.add(didfComboType);
    }

    return validTypes;
}