List of usage examples for java.lang String CASE_INSENSITIVE_ORDER
Comparator CASE_INSENSITIVE_ORDER
To view the source code for java.lang String CASE_INSENSITIVE_ORDER.
Click Source Link
From source file:com.hichinaschool.flashcards.anki.CardEditor.java
private void actualizeTagDialog(StyledDialog ad) { TreeSet<String> tags = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); for (String tag : mCol.getTags().all()) { tags.add(tag);//from w w w . j a va2s .c om } tags.addAll(selectedTags); int len = tags.size(); allTags = new String[len]; boolean[] checked = new boolean[len]; int i = 0; for (String t : tags) { allTags[i++] = t; if (selectedTags.contains(t)) { checked[i - 1] = true; } } ad.setMultiChoiceItems(allTags, checked, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface arg0, int which) { String tag = allTags[which]; if (selectedTags.contains(tag)) { // Log.i(AnkiDroidApp.TAG, "unchecked tag: " + tag); selectedTags.remove(tag); } else { // Log.i(AnkiDroidApp.TAG, "checked tag: " + tag); selectedTags.add(tag); } } }); }
From source file:weave.servlets.AdminService.java
/** * getSortedUniqueValues/*from w w w . j a v a2s .co m*/ * * @param values * A list of string values which may contain duplicates. * @param moveEmptyStringToEnd * If set to true and "" is at the front of the list, "" is moved * to the end. * @return A sorted list of unique values found in the given list. */ private List<String> getSortedUniqueValues(List<String> values, boolean moveEmptyStringToEnd) { Set<String> uniqueValues = new HashSet<String>(); uniqueValues.addAll(values); Vector<String> result = new Vector<String>(uniqueValues); Collections.sort(result, String.CASE_INSENSITIVE_ORDER); // if empty string is at beginning of sorted list, move it to the end of // the list if (moveEmptyStringToEnd && result.size() > 0 && result.get(0).equals("")) result.add(result.remove(0)); return result; }
From source file:com.mirth.connect.server.controllers.DefaultConfigurationController.java
private void saveConfigurationProperties(Map<String, ConfigurationProperty> map) throws ControllerException { try {/* www . ja va 2 s. co m*/ PropertiesConfiguration configurationMapProperties = new PropertiesConfiguration(); configurationMapProperties.setDelimiterParsingDisabled(true); configurationMapProperties.setListDelimiter((char) 0); configurationMapProperties.clear(); PropertiesConfigurationLayout layout = configurationMapProperties.getLayout(); Map<String, ConfigurationProperty> sortedMap = new TreeMap<String, ConfigurationProperty>( String.CASE_INSENSITIVE_ORDER); sortedMap.putAll(map); for (Entry<String, ConfigurationProperty> entry : sortedMap.entrySet()) { String key = entry.getKey(); String value = entry.getValue().getValue(); String comment = entry.getValue().getComment(); if (StringUtils.isNotBlank(key)) { configurationMapProperties.addProperty(key, value); layout.setComment(key, StringUtils.isBlank(comment) ? null : comment); } } configurationMapProperties.save(new File(configurationFile)); } catch (Exception e) { throw new ControllerException(e); } }
From source file:org.apache.usergrid.persistence.Schema.java
/** @return entity properties from columns as a map */ public static Map<String, Object> deserializeEntityProperties(Map<String, ByteBuffer> columns, boolean checkId, boolean checkRequired) { if (columns == null) { return null; }//from w w w.ja v a 2 s.co m String entityType = string(columns.get(PROPERTY_TYPE)); if (entityType == null) { logger.debug("deserializeEntityProperties(): No type for entity found, entity probably doesn't exist"); return null; } if (checkId && !columns.containsKey(PROPERTY_UUID)) { logger.error("No id for entity ( {} ) found!", entityType); return null; } if (checkRequired) { Set<String> required_properties = Schema.getDefaultSchema().getRequiredProperties(entityType); if (required_properties != null) { for (String property_name : required_properties) { if (!columns.containsKey(property_name)) { logger.error("Entity (" + entityType + ") missing required property: " + property_name, new Throwable()); return null; } } } } Map<String, Object> properties_map = new TreeMap<String, Object>(String.CASE_INSENSITIVE_ORDER); for (Entry<String, ByteBuffer> column : columns.entrySet()) { String propertyName = column.getKey(); Object propertyValue = deserializeEntityProperty(entityType, propertyName, column.getValue()); properties_map.put(propertyName, propertyValue); } return properties_map; }
From source file:com.afwsamples.testdpc.policy.PolicyManagementFragment.java
/** * Shows a list of account types that is disabled for account management. *//*from w w w . ja va 2 s.c o m*/ private void showDisableAccountTypeList() { if (getActivity() == null || getActivity().isFinishing()) { return; } String[] disabledAccountTypeList = mDevicePolicyManager.getAccountTypesWithManagementDisabled(); Arrays.sort(disabledAccountTypeList, String.CASE_INSENSITIVE_ORDER); if (disabledAccountTypeList == null || disabledAccountTypeList.length == 0) { showToast(R.string.no_disabled_account); } else { new AlertDialog.Builder(getActivity()).setTitle(R.string.list_of_disabled_account_types) .setAdapter(new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, android.R.id.text1, disabledAccountTypeList), null) .setPositiveButton(android.R.string.ok, null).show(); } }
From source file:pcgen.core.GameMode.java
public void addHiddenType(Class<?> cl, String s) { Set<String> set = hiddenTypes.computeIfAbsent(cl, k -> new TreeSet<>(String.CASE_INSENSITIVE_ORDER)); set.add(s);/*w ww. j a va2 s .c o m*/ }
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. * //w w w . jav a 2s . 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.alfresco.repo.security.sync.ChainingUserRegistrySynchronizer.java
/** * Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling * deletions./* w ww.ja v a 2s . 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.cggh.repo.security.sync.CustomChainingUserRegistrySynchronizer.java
/** * Synchronizes local groups and users with a {@link UserRegistry} for a particular zone, optionally handling * deletions.//from www . j av a 2 s . 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); }