View Javadoc
1   package edu.internet2.middleware.grouper.pspng;
2   
3   /*******************************************************************************
4    * Copyright 2015 Internet2
5    * 
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    * 
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   ******************************************************************************/
18  
19  import java.util.ArrayList;
20  import java.util.Arrays;
21  import java.util.Collection;
22  import java.util.Collections;
23  import java.util.Date;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Map.Entry;
30  import java.util.Set;
31  import java.util.concurrent.Callable;
32  import java.util.concurrent.ExecutionException;
33  import java.util.concurrent.ExecutorService;
34  import java.util.concurrent.Executors;
35  import java.util.concurrent.Future;
36  import java.util.concurrent.ThreadFactory;
37  import java.util.concurrent.atomic.AtomicInteger;
38  import java.util.concurrent.atomic.AtomicReference;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  
42  import org.apache.commons.lang.BooleanUtils;
43  import org.apache.commons.lang.StringUtils;
44  import org.apache.log4j.MDC;
45  import org.joda.time.DateTime;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  import edu.internet2.middleware.grouper.Group;
50  import edu.internet2.middleware.grouper.GroupFinder;
51  import edu.internet2.middleware.grouper.GrouperSession;
52  import edu.internet2.middleware.grouper.Member;
53  import edu.internet2.middleware.grouper.Stem;
54  import edu.internet2.middleware.grouper.Stem.Scope;
55  import edu.internet2.middleware.grouper.StemFinder;
56  import edu.internet2.middleware.grouper.StemSave;
57  import edu.internet2.middleware.grouper.SubjectFinder;
58  import edu.internet2.middleware.grouper.app.loader.GrouperLoaderConfig;
59  import edu.internet2.middleware.grouper.attr.AttributeDef;
60  import edu.internet2.middleware.grouper.attr.AttributeDefType;
61  import edu.internet2.middleware.grouper.attr.AttributeDefValueType;
62  import edu.internet2.middleware.grouper.audit.GrouperEngineBuiltin;
63  import edu.internet2.middleware.grouper.cache.GrouperCache;
64  import edu.internet2.middleware.grouper.cfg.GrouperConfig;
65  import edu.internet2.middleware.grouper.changeLog.ChangeLogEntry;
66  import edu.internet2.middleware.grouper.changeLog.ChangeLogLabels;
67  import edu.internet2.middleware.grouper.changeLog.ChangeLogTypeBuiltin;
68  import edu.internet2.middleware.grouper.exception.GroupNotFoundException;
69  import edu.internet2.middleware.grouper.hibernate.GrouperContext;
70  import edu.internet2.middleware.grouper.internal.dao.QueryOptions;
71  import edu.internet2.middleware.grouper.misc.GrouperCheckConfig;
72  import edu.internet2.middleware.grouper.misc.GrouperDAOFactory;
73  import edu.internet2.middleware.grouper.pit.PITGroup;
74  import edu.internet2.middleware.grouper.pit.finder.PITGroupFinder;
75  import edu.internet2.middleware.grouper.util.GrouperUtil;
76  import edu.internet2.middleware.grouperClient.collections.MultiKey;
77  import edu.internet2.middleware.grouperClient.jdbc.GcDbAccess;
78  import edu.internet2.middleware.grouperClient.util.ExpirableCache;
79  import edu.internet2.middleware.subject.Subject;
80  import edu.internet2.middleware.subject.provider.SubjectTypeEnum;
81  
82  
83  /**
84   * Top-Level provisioner class of PSPNG and is the superclass of Target-System-Specific 
85   * subclasses. This class is generic so that User & Group objects that the subclasses
86   * need are cached and referenced directly in overridden methods. For example, an
87   * LDAP-provisioning subclass will receive both Subject and LdapUser information.
88   * 
89   * This class is responsible for the following functionalities:
90   *   -Mapping the grouper world of Subjects & Groups into User and Group objects specific to the target system
91   *   -Caching both Grouper & Target-System information
92   *   -Driving Incremental and FullSync operations
93   *   -Keep track of what groups need to be full-synced (ie, there is a full-sync queue)
94   * 
95   * While there are several abstract methods:
96   *     fetchTargetSystemUsers & fetchTargetSystemGroups
97   *     addMembership & deleteMembership
98   *     createGroup & deleteGroup
99   *     doFullSync & doFullSync_cleanupExtraGroups
100  *   
101  * And that some subclasses can override the following method(s):
102  *     createUser
103  *   
104  *   
105  * The Provisioner lifecycle is as follows:
106  * -Constructed(Name and ProvisionerConfiguration subclass)
107  *   (setup empty caches)
108  * 
109  * Invoked with a batch of changes
110  *   -startProvisioningBatch(A collection of change events)
111  *      (refresh caches as necessary, do any bulk fetching that will help provisioning go faster)
112  *   -provisionItem or addMember/deleMembership/createGroup/deleteGroup (for each event in the batch)
113  *      (Determine and/or make the necessary changes)
114  *      
115  *   -finishProvisioningBatch(A collection of change events)
116  *      (Perform any batch updates that were determined, but not performed, by provisionItem step)
117  *      (Clear any caches specific to batch)
118  *      
119  *  FullSync
120  *    -Go through all Grouper groups and see which match the provisioner's selection filter
121  *    -Run the subclass's doFullSync method for each group that matches
122  *    -Run doFullSync_cleanupExtraGruops with the list of correct groups
123  *      
124  * @author Bert Bee-Lindgren
125  *
126  */
127 public abstract class Provisioner
128   <ConfigurationClass extends ProvisionerConfiguration, 
129    TSUserClass extends TargetSystemUser, 
130    TSGroupClass extends TargetSystemGroup> {
131   private static final String DO_NOT_PROVISION_TO_ATTRIBUTE = "do_not_provision_to";
132 
133   private static final String PROVISION_TO_ATTRIBUTE = "provision_to";
134 
135   static final Logger STATIC_LOG = LoggerFactory.getLogger(Provisioner.class);
136   
137   protected final Logger LOG;
138 
139   /**
140    * job stats for real time
141    */
142   private JobStatistics jobStatistics;
143 
144   /**
145    * job stats for real time
146    * @return
147    */
148   public JobStatistics getJobStatistics() {
149     return jobStatistics;
150   }
151 
152   /**
153    * job stats for real time
154    * @param jobStatistics
155    */
156   public void setJobStatistics(JobStatistics jobStatistics) {
157     this.jobStatistics = jobStatistics;
158   }
159 
160   // What should logs show for this provisioner's name. This differentiates between the
161   // Incremental/Normal version of the provisioner and its full-sync version
162   final public String provisionerDisplayName;
163 
164   // What config elements underpin this provisioner, both in grouper_loader.properties
165   // and in Grouper Attributes. This will be the same for both the Incremental and the
166   // full-sync provisioners
167   final public String provisionerConfigName;
168   
169   // Cache groups by groupInfo key
170   final GrouperCache<String, GrouperGroupInfo> grouperGroupInfoCache;
171   
172   // Cache subjects by sourceId__subjectId key
173   final GrouperCache<String, Subject> grouperSubjectCache;
174 
175   // Cache TargetSystemUsers by Subject. This is typically a long-lived cache
176   // used across provisioning batches. These are only fetched and cached
177   // if our config has needsTargetSystemUsers=true.
178   final PspDatedCache<Subject, TSUserClass> targetSystemUserCache;
179   
180   // This stores TargetSystemUserss during the provisioning batch. This Map might seem
181   // redundant to the targetSystemUserCache, but it is needed for
182   // two reasons: 1) To make sure items are not flushed during the provisioning batch
183   // like they could be within a GrouperCache; 2) To know if a TSUserClass really
184   // doesn't exist (getTargetSystemUser might do a lookup every time a nonexistent
185   // user is requested. By using a map populated once per provisioning batch, we don't
186   // have to do extra lookups during the batch).
187   // If our config has needsTargetSystemUsers=true, this map is populated by 
188   // startProvisioningBatch and emptied by finishProvisioningBatch
189   private Map<Subject, TSUserClass> tsUserCache_shortTerm = new HashMap<Subject, TSUserClass>();
190   
191   
192   // Cache TargetSystemGroups by Group. This is a long-lived cache, typically used across
193   // several provisioning batches. These are only fetched and cached if our config
194   // has needsTargetSystemGroups=true.
195   final GrouperCache<GrouperGroupInfo, TSGroupClass> targetSystemGroupCache;
196   
197   // This stores TargetSystemGroups during the provisioning batch. This Map might seem
198   // redundant to the targetSystemGroupCache, but it is needed for
199   // two reasons: 1) To make sure items are not flushed during the provisioning batch
200   // like they could be within a GrouperCache; 2) To know if a TSGroupClass really
201   // doesn't exist (a GrouperCache doesn't differentiate between an uncached value and
202   // a nonexistent group. By using a map populated once per provisioning batch, we don't
203   // do futile lookups during the batch).
204   //
205   // This map is populated by startProvisioningBatch and emptied by finishProvisioningBatch
206   // if our config has needsTargetSystemGroups=true.
207   private Map<GrouperGroupInfo, TSGroupClass> tsGroupCache_shortTerm = new HashMap<GrouperGroupInfo, TSGroupClass>();
208 
209   // This is used during provisioning so everyone can get access to the current
210   // work item while we're looping through the work items of a batch
211   private ThreadLocal<ProvisioningWorkItem> currentWorkItem = new ThreadLocal<ProvisioningWorkItem>();
212 
213 
214   // This is used to track what groups are selected for this provisioner and used
215   // when groups are deleted (to know if they were selected before they were deleted)
216   // or when attributes change (to know if new groups are selected or if old ones
217   // are no longer selected)
218   private AtomicReference<Set<GrouperGroupInfo>> selectedGroups = new AtomicReference<>();
219 
220   /**
221    * Should this provisioner operate in Full-Sync mode? This might mean fetching all members of a group
222    * which can be expensive in an incremental-sync, but is worth the trouble in a full-sync.
223    */
224   protected final boolean fullSyncMode;
225   
226   final protected ConfigurationClass config;
227 
228   final private ExecutorService tsUserFetchingService;
229 
230   // This is managed during incremental and full provisioning activities
231   public static final ThreadLocal<Provisioner>  activeProvisioner = new ThreadLocal<>();
232 
233   
234   Provisioner(String provisionerConfigName, ConfigurationClass config, boolean fullSyncMode) {
235     this.provisionerConfigName = provisionerConfigName;
236 
237     if (fullSyncMode) {
238       this.provisionerDisplayName = provisionerConfigName + "-full";
239     } else {
240       this.provisionerDisplayName = provisionerConfigName;
241     }
242     this.fullSyncMode = fullSyncMode;
243     LOG = LoggerFactory.getLogger(String.format("%s.%s", getClass().getName(), provisionerDisplayName));
244 
245     this.config = config;
246 
247     checkAttributeDefinitions();
248 
249     // NOTE: PSPNG has different size=0 semantics... In PSPNG, size=0 means zero caching, but in
250     // GrouperCache/EhCache size=0 means unlimited caching. Therefore, we map size=0 to size=1.
251     grouperGroupInfoCache
252             = new GrouperCache<String, GrouperGroupInfo>(String.format("PSP-%s-GrouperGroupInfoCache", getConfigName()),
253             config.getGrouperGroupCacheSize() > 1 ? config.getGrouperGroupCacheSize() : 1,
254             false,
255             config.getDataCacheTime_secs(),
256             config.getDataCacheTime_secs(),
257             false);
258 
259     grouperSubjectCache
260               = new GrouperCache<String, Subject>(String.format("PSP-%s-GrouperSubjectCache", getConfigName()),
261               config.getGrouperSubjectCacheSize() > 1 ? config.getGrouperSubjectCacheSize() : 1,
262               false,
263               config.getDataCacheTime_secs(),
264               config.getDataCacheTime_secs(),
265               false);
266 
267     targetSystemUserCache
268               = new PspDatedCache<Subject, TSUserClass>(String.format("PSP-%s-TargetSystemUserCache", getConfigName()),
269               config.getTargetSystemUserCacheSize() > 1 ? config.getTargetSystemUserCacheSize() : 1,
270               false,
271               config.getDataCacheTime_secs(),
272               config.getDataCacheTime_secs(),
273               false);
274 
275     // This cache is set up with DisplayName of provisioner
276     // to keep the caches of the FullSync and Incremental instances separate
277     targetSystemGroupCache
278       = new GrouperCache<GrouperGroupInfo, TSGroupClass>(String.format("PSP-%s-TargetSystemGroupCache", getDisplayName()),
279           config.getTargetSystemGroupCacheSize() > 1 ? config.getTargetSystemGroupCacheSize() : 1,
280           false,
281           config.getDataCacheTime_secs(),
282           config.getDataCacheTime_secs(),
283           false);
284 
285     if ( config.needsTargetSystemUsers() ) {
286       tsUserFetchingService = Executors.newFixedThreadPool(
287               config.getNumberOfDataFetchingWorkers(),
288               new ThreadFactory() {
289                 AtomicInteger counter = new AtomicInteger(1);
290                 @Override
291                 public Thread newThread(Runnable r) {
292                   Thread thread = new Thread(r, String.format("TSUserFetcher-%s-%d", getDisplayName(), counter.getAndIncrement()));
293                   thread.setDaemon(true);
294                   return thread;
295                 }
296               });
297     } else {
298       tsUserFetchingService=null;
299     }
300 
301     selectedGroups.set(getAllGroupsForProvisioner());
302   }
303   
304 
305   /**
306    * This creates any attributes missing within the etc:pspng: folder.
307    */
308   public static void checkAttributeDefinitions() {
309     GrouperSession grouperSession = GrouperSession.staticGrouperSession();
310     if ( grouperSession == null )
311       grouperSession = GrouperSession.startRootSession();
312     
313     //GRP-1356: pspng should use the default configuration folder
314     //String pspngManagementStemName = "etc:pspng";
315     String pspngManagementStemName = GrouperConfig.retrieveConfig().propertyValueString(
316         "grouper.rootStemForBuiltinObjects", "etc") + ":pspng";
317     
318     Stem pspngManagementStem = StemFinder.findByName(grouperSession, pspngManagementStemName, false);
319     if (pspngManagementStem == null) {
320       pspngManagementStem = new StemSave(grouperSession).assignCreateParentStemsIfNotExist(true)
321         .assignDescription("Location for pspng-management objects.")
322         .assignName(pspngManagementStemName).save();
323     }
324 
325     //see if provision_to_def attributeDef is there
326     String provisionToDefName = pspngManagementStemName + ":" + PROVISION_TO_ATTRIBUTE + "_def";
327     AttributeDef provisionToDef = GrouperDAOFactory.getFactory().getAttributeDef().findByNameSecure(
328         provisionToDefName, false, new QueryOptions().secondLevelCache(false));
329     if (provisionToDef == null) {
330       provisionToDef = pspngManagementStem.addChildAttributeDef(PROVISION_TO_ATTRIBUTE + "_def", AttributeDefType.type);
331       provisionToDef.setAssignToGroup(true);
332       provisionToDef.setAssignToStem(true);
333       provisionToDef.setMultiAssignable(true);
334       provisionToDef.setValueType(AttributeDefValueType.string);
335       provisionToDef.store();
336     }
337     
338     //see if do_not_provision_to_def attributeDef is there
339     String doNotProvisionToDefName = pspngManagementStemName + ":" + DO_NOT_PROVISION_TO_ATTRIBUTE + "_def";
340     AttributeDef doNotProvisionToDef = GrouperDAOFactory.getFactory().getAttributeDef().findByNameSecure(
341         doNotProvisionToDefName, false, new QueryOptions().secondLevelCache(false));
342     if (doNotProvisionToDef == null) {
343       doNotProvisionToDef = pspngManagementStem.addChildAttributeDef(DO_NOT_PROVISION_TO_ATTRIBUTE+"_def", AttributeDefType.type);
344       doNotProvisionToDef.setAssignToGroup(true);
345       doNotProvisionToDef.setAssignToStem(true);
346       doNotProvisionToDef.setMultiAssignable(true);
347       doNotProvisionToDef.setValueType(AttributeDefValueType.string);
348       doNotProvisionToDef.store();
349     }
350     
351     GrouperCheckConfig.checkAttribute(pspngManagementStem, provisionToDef, PROVISION_TO_ATTRIBUTE, PROVISION_TO_ATTRIBUTE, "Defines what provisioners should process a group or groups within a folder", true);
352     GrouperCheckConfig.checkAttribute(pspngManagementStem, doNotProvisionToDef, DO_NOT_PROVISION_TO_ATTRIBUTE, DO_NOT_PROVISION_TO_ATTRIBUTE, "Defines what provisioners should not process a group or groups within a folder. Since the default is already for provisioners to not provision any groups, this attribute is to override a provision_to attribute set on an ancestor folder. ", true);
353   }
354 
355 
356   /**
357    * Action method that handles membership additions where a person-subject is added to a 
358    * group. The top-level Provisioner class implementation is abstract, and, of course, 
359    * this method is expected to be overridden by every provisioner subclass to accomplish 
360    * something useful. 
361    * 
362    * @param grouperGroupInfo The group to which the subject needs to be added as a member
363    * @param tsGroup A TSGroupClass created for group by fetchTargetSystemGroup. This will
364    * be null for systems that do not need target system groups.
365    * @param subject The (person) subject that needs to be provisioned as a member of 'group'
366    * @param tsUser A TSUserClass created for the subject by fetchTargetSystemUser. This will
367    * be null for systems that do not need target system users.
368    */
369 
370   protected abstract void addMembership(GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup,
371       Subject subject, TSUserClass tsUser) throws PspException;
372   
373   
374   /**
375    * Abstract action method that handles membership removals. 
376    * 
377    * Note: This method is called for MembershipDelete events for a non-group member.
378    * 
379    * @param grouperGroupInfo The group to which the subject needs to be removed as a member
380    * @param tsGroup TSGroupClass for the 'group.' This is null for systems that do not need
381    * target-system group info
382    * @param subject The subject that needs to be deprovisioned as a member of 'group'
383    * @param tsUser TSUserClass for the 'subject.' This is null for systems that do not need
384    * target-system user info
385    */
386 
387   protected abstract void deleteMembership(GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup,
388       Subject subject, TSUserClass tsUser) throws PspException;
389 
390   
391   /**
392    * Provisioning a new Group in the target system. This must be overridden in provisioner
393    * subclasses that support creating groups. This will normally be called (with an empty
394    * members parameter) when provisioning-enabled groups are created in Grouper. However, 
395    * when an empty group cannot be created (eg, an ldap group that _requires_ members) or when
396    * members are somehow already known, this  may be called with a (non-empty) list of members.
397    * 
398    * @param grouperGroup
399    * @param initialMembers What members should in the provisioned group once the method completes. 
400    * This is generally empty during incremental/changelog-based provisioning, but may list users 
401    * at other times.
402    * @return
403    * @throws PspException
404    */
405   protected abstract TSGroupClass createGroup(GrouperGroupInfo grouperGroup, Collection<Subject> initialMembers) throws PspException;
406 
407   /**
408    * Action method that handles group removal. The top-level Provisioner class implementation
409    * does nothing except log an error if the target system needs groups.
410    * 
411    * This is expected to be overridden by subclasses if the target system needs groups, and
412    * do not call the super.deleteGroup version of this when you override it this
413    * @param grouperGroupInfo
414    * @param tsGroup 
415    */
416   protected abstract void 
417   deleteGroup(GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup) throws PspException;
418   
419   /**
420    * This method's responsibility is to make sure that group's only provisioned memberships are those
421    * of correctSubjects. Extra subjects should be removed. 
422    * 
423    * Before this is called, the following have occurred:
424    *   -a ProvisioningWorkItem was created representing the whole Full Sync, and it was marked
425    *    as the current provisioning item
426    *   -StartProvisioningBatch was called
427    *   -TSGroupClass- and TSUserClass-caches are populated with the group and CORRECT Subjects
428    *   
429    * Also, remember that fullSyncMode=true for provisioners doing full-sync, so TargetSystemUsers and
430    * TargetSystemGroups should have the extra information needed to facilitate full syncs.
431    * 
432    * @param grouperGroupInfo Grouper group to fully synchronize with target system
433    * @param tsGroup TSGroupClass that maps to group.
434    * @param correctSubjects What subjects are members in the Grouper Registry
435    * @param tsUserMap Map of TargetSystemUsers which map to the correctSubjects. This will be empty
436    * for provisioners that do not use TargetSystemUsers.
437    * @param correctTSUsers A list of the TSUsers that correspond to correctSubjects. This might be a subset
438    * of the TSUsers in the tsUserMap.
439    * @param stats A holder of the number of changes the fullSync performs
440    * @return True when changes were made to target system
441    */
442   protected abstract boolean doFullSync(
443       GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup, 
444       Set<Subject> correctSubjects, Map<Subject, TSUserClass> tsUserMap, Set<TSUserClass> correctTSUsers,
445       JobStatistics stats)
446           throws PspException;
447 
448   /**
449    * This method's responsibility is find extra groups within Grouper's responsibility that
450    * exist in the target system. These extra groups should be removed.
451    * 
452    * Note: This is only called when grouperIsAuthoritative=true in the provisioner's properties.
453    * 
454    * To avoid deleting newly created groups, implementations should follow the following:
455    *   1) Read all the provisioned information
456    *   2) Read all the groups that should be provisioned (getAllGroupsForProvisioner())
457    *   3) Fetch group information from system (fetchTargetSystemGroupsInBatches(allGroupsForProvisioner)
458    *      (if TSGroup objects are relevant)
459    *   4) Compare (1) & (3) and delete anything extra in (1)
460    * 
461    */
462   protected abstract void doFullSync_cleanupExtraGroups(JobStatistics stats) throws PspException;
463   
464 
465   /**
466    * This fetches group information from the target system. Subclasses that have TSGroupClass 
467    * information need to override this. Subclasses that do not need TSGroupClass information
468    * should just return either an empty map (Collections.EMPTY_MAP) or null;. 
469    * 
470    * Note:
471    * The signature of this method is designed for batch fetching. If you cannot fetch batches of
472    * information, then loop through the provided groups and build a resulting map.
473    * @param grouperGroups
474    * @return
475    * @throws PspException
476    */
477   protected abstract Map<GrouperGroupInfo, TSGroupClass> 
478   fetchTargetSystemGroups(Collection<GrouperGroupInfo> grouperGroups) throws PspException;
479 
480 
481   /**
482    * This fetches user information from the target system. Subclasses that have TSUserClass 
483    * information need to override this. Subclasses that do not have TSUserClass should implement
484    * this so it returns either an empty map (Collections.EMPTY_MAP) or null.
485    * 
486    * Note:
487    * The signature of this method is designed for batch fetching. If you cannot fetch batches of
488    * information, then loop through the provided users and build a resulting map.
489    * 
490    * @param personSubjects 
491    * @return
492    * @throws PspException
493    */
494   protected abstract Map<Subject, TSUserClass> 
495   fetchTargetSystemUsers(Collection<Subject> personSubjects) throws PspException;
496 
497   /**
498    * cache for two minutes decisions about if provisionable
499    */
500   private static Map<String, ExpirableCache<String, MultiKey>> configIdToGroupNameToMillisAndProvisionable = new HashMap<String, ExpirableCache<String, MultiKey>>();
501 
502   public static ExpirableCache<String, MultiKey> groupNameToMillisAndProvisionable(String provisionerConfigId) {
503     
504     ExpirableCache<String, MultiKey> cache = configIdToGroupNameToMillisAndProvisionable.get(provisionerConfigId);
505     if (cache == null) {
506       cache = new ExpirableCache<String, MultiKey>(10);
507       configIdToGroupNameToMillisAndProvisionable.put(provisionerConfigId, cache);
508     }
509     
510     return cache;
511   }
512   
513   
514   /**
515    * This method returns the work items that are supposed to be provisioned
516    * by calling shouldGroupBeProvisioned on each group mentioned
517    * by a workItem. If a workItem's group is within the scope of this provisioner
518    * or if the workItem is not related to a group, then it is included in the
519    * returned list of work items that should be processed further. Otherwise, it
520    * is marked as completed and not returned.
521    * 
522    * If the workItem is not to be processed, 
523    * @param workItems WorkItems read from the triggering source (Changelog or messaging).
524    * This will include both events that affect groups and those that do not. Generally, we
525    * pass on non-group changes in case a provisioner wants to process them. 
526    * @return The list of workItems that are to be provisioned
527    * @throws PspException
528    */
529   public List<ProvisioningWorkItem> 
530   filterWorkItems2(List<ProvisioningWorkItem> workItems) throws PspException {
531 
532     List<ProvisioningWorkItem> result = new ArrayList<ProvisioningWorkItem>();
533     
534     LOG.debug("Filtering provisioning batch of {} items", workItems.size());
535 
536     boolean pspngCacheGroupProvisionable = GrouperLoaderConfig.retrieveConfig().propertyValueBoolean("pspngCacheGroupProvisionable", true);
537 
538     Set<String> attributesUsedInProvisioning = new HashSet<String>(GrouperUtil.nonNull(getConfig().getAttributesUsedInGroupSelectionExpression()));
539     
540     ExpirableCache<String, MultiKey> groupNameToMillisAndProvisionable = groupNameToMillisAndProvisionable(this.provisionerConfigName);
541     
542     for ( ProvisioningWorkItem workItem : workItems ) {
543       
544       String groupNameForCache = null;
545       Boolean provisionableFromCache = null;
546       if (pspngCacheGroupProvisionable) {
547         groupNameForCache = workItem.getGroupName();
548         
549         final String attributeName = workItem.getAttributeName();
550 
551         // if this change log entry is an attribute item and the attribute is a provision attribute
552         if (!StringUtils.isBlank(attributeName) && attributesUsedInProvisioning.contains(attributeName)) {
553           
554           // clear this if attributes are being changed
555           groupNameToMillisAndProvisionable.clear();
556         } else {
557 
558           // if this change log has a group associated
559           if (!StringUtils.isBlank(groupNameForCache)) {
560 
561             // cache entry
562             MultiKey millisProvisionable = groupNameToMillisAndProvisionable.get(groupNameForCache);
563             ChangeLogEntry changeLogEntry = workItem.getChangelogEntry();
564             
565             // when this change log entry happened
566             Long millisFromChangeLogEntry = changeLogEntry == null ? null : (changeLogEntry.getCreatedOnDb() / 1000);
567             
568             if (millisProvisionable != null && millisFromChangeLogEntry != null) {
569               
570               // when the cached isProvisionable calculation happened
571               long millisProvisionableCacheDecision = (Long)millisProvisionable.getKey(0);
572               
573               // if we have a more recent cached provisionable decision
574               if (millisFromChangeLogEntry < millisProvisionableCacheDecision) {
575                 provisionableFromCache = (Boolean)millisProvisionable.getKey(1);
576 
577                 // if this is irrelevant, then skip
578                 if (!provisionableFromCache) {
579                   workItem.markAsSkipped("Ignoring work item due to cached decision");
580                   continue;
581                 }
582                 // if group is provisionable still do logic below
583               }
584             }
585           }
586         }
587       }
588       
589       GrouperGroupInfo group = workItem.getGroupInfo(this);
590       final long millisWhenProvisionableDecisionMade = System.currentTimeMillis();
591 
592       // Groups that haven't been deleted: Skip them if they're not supposed to be provisioned
593       // if we have a cached answer then dont do this
594       if ( group != null && provisionableFromCache == null ) {
595         if ( !group.hasGroupBeenDeleted() && !shouldGroupBeProvisioned(group)) {
596           workItem.markAsSkipped("Ignoring work item because (existing) group should not be provisioned");
597           
598           // cache that this is irrelevant
599           if (!StringUtils.isBlank(groupNameForCache)) {
600             groupNameToMillisAndProvisionable.put(groupNameForCache, new MultiKey(millisWhenProvisionableDecisionMade, false));
601           }
602           continue;
603         }
604 
605         if ( group.hasGroupBeenDeleted() && !selectedGroups.get().contains(group) ) {
606           workItem.markAsSkippedAndWarn("Ignoring work item because (deleted) group was not provisioned before it was deleted");
607           // cache that this is irrelevant
608           if (!StringUtils.isBlank(groupNameForCache)) {
609             groupNameToMillisAndProvisionable.put(groupNameForCache, new MultiKey(millisWhenProvisionableDecisionMade, false));
610           }
611           continue;
612         }
613       }
614 
615       if (!StringUtils.isBlank(groupNameForCache) && provisionableFromCache == null) {
616         // this means group is provisionable, but still do shouldWorkItemBeProcessed()
617         groupNameToMillisAndProvisionable.put(groupNameForCache, new MultiKey(millisWhenProvisionableDecisionMade, true));
618       }
619       
620       // check to see if the subject is applicable etc
621       if ( shouldWorkItemBeProcessed(workItem) ) {
622         result.add(workItem);
623       } else {
624         // dont change group provisionable cache, indeterminate
625         // Not going to process this item, so mark it as a success and don't add it to result
626         workItem.markAsSkipped("Ignoring work item");
627       }
628     }
629     
630     return result;
631   }
632   /**
633    * This method returns the work items that are supposed to be provisioned
634    * by calling shouldGroupBeProvisioned on each group mentioned
635    * by a workItem. If a workItem's group is within the scope of this provisioner
636    * or if the workItem is not related to a group, then it is included in the
637    * returned list of work items that should be processed further. Otherwise, it
638    * is marked as completed and not returned.
639    * 
640    * If the workItem is not to be processed, 
641    * @param workItems WorkItems read from the triggering source (Changelog or messaging).
642    * This will include both events that affect groups and those that do not. Generally, we
643    * pass on non-group changes in case a provisioner wants to process them. 
644    * @return The list of workItems that are to be provisioned
645    * @throws PspException
646    */
647   public List<ProvisioningWorkItem> 
648   filterWorkItems(List<ProvisioningWorkItem> workItems) throws PspException {
649     
650     boolean pspngCacheGroupProvisionable = GrouperLoaderConfig.retrieveConfig().propertyValueBoolean("pspngCacheGroupProvisionable", true);
651     if (pspngCacheGroupProvisionable) {
652       return filterWorkItems2(workItems);
653     }
654     List<ProvisioningWorkItem> result = new ArrayList<ProvisioningWorkItem>();
655     
656     LOG.debug("Filtering provisioning batch of {} items", workItems.size());
657 
658     for ( ProvisioningWorkItem workItem : workItems ) {
659       GrouperGroupInfo group = workItem.getGroupInfo(this);
660 
661       // Groups that haven't been deleted: Skip them if they're not supposed to be provisioned
662       if ( group != null ) {
663         if ( !group.hasGroupBeenDeleted() && !shouldGroupBeProvisioned(group)) {
664           workItem.markAsSkipped("Ignoring work item because (existing) group should not be provisioned");
665           continue;
666         }
667 
668         if ( group.hasGroupBeenDeleted() && !selectedGroups.get().contains(group) ) {
669           workItem.markAsSkippedAndWarn("Ignoring work item because (deleted) group was not provisioned before it was deleted");
670           continue;
671         }
672       }
673 
674       if ( shouldWorkItemBeProcessed(workItem) ) {
675         result.add(workItem);
676       } else {
677         // Not going to process this item, so mark it as a success and don't add it to result
678           workItem.markAsSkipped("Ignoring work item");
679       }
680     }
681     
682     return result;
683   }
684 
685   /**
686    * Used to filter workItems. This can be overridden by a subclass that was unhappy
687    * with the default filter behaviors. If overriding this, eg, to accept additional
688    * changes into a provisioner, then it would probably be useful to look for those
689    * additional types of changes and then call the super version of this.
690    *
691    * Note: In addition to returning a boolean about skipping the work item, it is helpful
692    * if this method actually calls markAsSkipped(reason) so that a more detailed
693    * reason can be noted for skipping it. In other words, this method has the
694    * (optional) side effect of actually marking the item as skipped. Finally, if this
695    * method does _not_ mark the item as skipped, it will be marked as skipped
696    * with a generic message.
697    *
698    * @param workItem
699    * @return
700    */
701   protected boolean shouldWorkItemBeProcessed(ProvisioningWorkItem workItem) {
702     if ( workItem.isSubjectUnresolvable(this) ) {
703       workItem.markAsSkippedAndWarn("Subject is unresolvable");
704       return false;
705     }
706 
707     // Check if we're configured to ignore changes to internal (g:gsa) subjects
708     // (default is that we do ignore such changes)
709     if ( getConfig().areChangesToInternalGrouperSubjectsIgnored() ) {
710       Subject subject = workItem.getSubject(this);
711       if ( subject != null && subject.getSourceId().equalsIgnoreCase("g:gsa") ) {
712         workItem.markAsSkipped("Ignoring event about a g:gsa subject");
713         return false;
714       }
715     }
716 
717     // Only the actual group-deletions are processed for groups that have been deleted
718     // This skips all the membership-removal changelog entries that precede the
719     // DElETE changelog entry
720     if ( workItem.getGroupInfo(this) != null &&
721          workItem.getGroupInfo(this).hasGroupBeenDeleted() ) {
722       if (workItem.matchesChangelogType(ChangeLogTypeBuiltin.GROUP_DELETE) ) {
723         LOG.debug("{}: Group has been deleted within grouper, so we're processing group-deletion event: {}", getDisplayName(), workItem);
724       } else {
725         workItem.markAsSkipped("Group has been deleted within Grouper. Skipping event because it is not the actual GROUP_DELETE event");
726         return false;
727       }
728     }
729 
730     if ( !workItem.matchesChangelogType(ChangelogHandlingConfig.allRelevantChangelogTypes) ) {
731       workItem.markAsSkipped("Changelog type is not relevant to PSPNG provisioning (see ChangelogHandlingConfig.allRelevantChangelogTypes)");
732       return false;
733     }
734 
735     return true;
736   }
737 
738 
739   /**
740    * Lock the groups that we are about to process. This will prevent simultaneous
741    * activity on them, which reduces the opportunities for duplicate provisioning operations
742    * that result in spurious error messages.
743    *
744    * @param workItems
745    */
746   public void startCoordination(List<ProvisioningWorkItem> workItems) {
747     for (ProvisioningWorkItem workItem : workItems) {
748       GrouperGroupInfo grouperGroupInfo = workItem.getGroupInfo(this);
749       if (grouperGroupInfo == null)
750         // Nothing to do before batch is processed
751         continue;
752 
753       if (isFullSyncMode()) {
754         getProvisionerCoordinator().lockForFullSyncIfNoIncrementalIsUnderway(grouperGroupInfo);
755       } else {
756         getProvisionerCoordinator().lockForIncrementalProvisioningIfNoFullSyncIsUnderway(grouperGroupInfo);
757       }
758     }
759   }
760 
761   /**
762    * Provisioning is over. Time to unlock in order to allow other full- or incremental-sync to
763    * occur on them
764    * @param workItems
765    * @param wasSuccessful
766    */
767   public void finishCoordination(List<ProvisioningWorkItem> workItems, boolean wasSuccessful) {
768 
769     for ( ProvisioningWorkItem workItem : workItems ) {
770       GrouperGroupInfo groupInfo = workItem.getGroupInfo(this);
771       if ( groupInfo != null ) {
772         if ( isFullSyncMode() ) {
773           getProvisionerCoordinator().unlockAfterFullSync(groupInfo, wasSuccessful);
774         }
775         else {
776           getProvisionerCoordinator().unlockAfterIncrementalProvisioning(groupInfo);
777         }
778       }
779     }
780   }
781 
782 
783   /**
784    * Get ready for a provisioning batch. If this is overridden, make sure you call super()
785    * at the beginning of your overridden version.
786    * 
787    * @param workItems
788    * @throws PspException
789    */
790   public void startProvisioningBatch(List<ProvisioningWorkItem> workItems) throws PspException {
791     Provisioner.activeProvisioner.set(this);
792     LOG.info("Starting provisioning batch of {} items", workItems.size());
793 
794     warnAboutCacheSizeConcerns();
795 
796     DateTime newestAsOfDate=null;
797     for ( ProvisioningWorkItem workItem : workItems) {
798       LOG.debug("-->Work item: {}", workItem);
799 
800       if ( workItem.asOfDate!=null ) {
801         if ( workItem.groupName != null ) {
802           DateTime lastFullSync = getFullSyncer().getLastSuccessfulFullSyncDate(workItem.groupName);
803           if ( lastFullSync!=null && lastFullSync.isAfter(workItem.asOfDate) ) {
804             workItem.markAsSkipped("Change was covered by full sync: {}",
805                     PspUtils.formatDate_DateHoursMinutes(lastFullSync,null));
806             continue;
807           }
808         }
809         // Is the workItem's asOfDate newer?
810         if (newestAsOfDate == null || newestAsOfDate.isBefore(workItem.asOfDate)) {
811           newestAsOfDate = workItem.asOfDate;
812         }
813       }
814     }
815 
816     LOG.info("Information cached before {} will be ignored", newestAsOfDate);
817 
818     Set<Subject> subjects = new HashSet<Subject>();
819 
820     // Use this Set to remove duplicate group names that are referenced in multiple workItems
821     Set<GrouperGroupInfo> grouperGroupInfos = new HashSet<GrouperGroupInfo>();
822 
823     for ( ProvisioningWorkItem workItem : workItems) {
824       GrouperGroupInfo grouperGroupInfo = workItem.getGroupInfo(this);
825       if ( grouperGroupInfo == null ) {
826         // Nothing to do before batch is processed
827         continue;
828       }
829 
830       grouperGroupInfos.add(grouperGroupInfo);
831 
832       Subject s = workItem.getSubject(this);
833       if ( s != null )
834         subjects.add(s);
835     }
836     
837     prepareGroupCache(grouperGroupInfos);
838     prepareUserCache(subjects, newestAsOfDate);
839   }
840 
841   protected void warnAboutCacheSizeConcerns() {
842     warnAboutCacheSizeConcerns(grouperGroupInfoCache.getStats().getObjectCount(), config.getGrouperGroupCacheSize(), "grouper groups", "grouperGroupCacheSize");
843     warnAboutCacheSizeConcerns(targetSystemGroupCache.getStats().getObjectCount(), config.getTargetSystemGroupCacheSize(), "provisioned groups", "targetSystemGroupCacheSize");
844     warnAboutCacheSizeConcerns(grouperSubjectCache.getStats().getObjectCount(), config.getGrouperSubjectCacheSize(), "grouper subjects", "grouperSubjectCacheSize");
845     warnAboutCacheSizeConcerns(targetSystemUserCache.getStats().getObjectCount(), config.getTargetSystemUserCacheSize(), "provisioned subjects", "targetSystemUserCacheSize");
846   }
847 
848   private void warnAboutCacheSizeConcerns(long cacheObjectCount, int configuredSize, String objectType, String configurationProperty) {
849     if ( !config.areCacheSizeWarningsEnabled() )
850       return;
851 
852     // If caching is disabled, then don't warn about it!!
853     if ( configuredSize <= 1 ) {
854       return;
855     }
856 
857     double cacheWarningThreshold_percentage = config.getCacheFullnessWarningThreshold_percentage();
858     long cacheWarningTreshold_count = Math.round(cacheWarningThreshold_percentage*configuredSize);
859 
860     long cacheFullness_percentage = Math.round(100.0*cacheObjectCount/configuredSize);
861 
862     if ( cacheFullness_percentage >= cacheWarningThreshold_percentage ) {
863       LOG.warn("{}: Cache of {} is very full ({}%). Provisioning performance is much better if {} is big enough to hold all {}. " +
864                       "({} is currently set to {}, and this warning occurs when cache is {}% full)",
865               getConfigName(),
866               objectType,
867               cacheFullness_percentage,
868               configurationProperty,
869               objectType,
870               configurationProperty, configuredSize,
871               cacheFullness_percentage);
872     } else {
873       LOG.debug("{}: Cache of {} is sufficiently sized ({}% full) (property {}={})",
874               getConfigName(), objectType, cacheFullness_percentage, configurationProperty, configuredSize);
875     }
876 
877   }
878 
879   private ProvisionerCoordinator getProvisionerCoordinator() {
880     return ProvisionerFactory.getProvisionerCoordinator(this);
881   }
882 
883   // Finish and/or clean up after a provisioning batch. If this is overridden, make sure you 
884   // call super() at the END of your overridden version
885   public void finishProvisioningBatch(List<ProvisioningWorkItem> workItems) throws PspException {
886     tsUserCache_shortTerm.clear();
887     tsGroupCache_shortTerm.clear();
888 
889     LOG.debug("Done with provisining batch");
890   }
891 
892   /**
893    * Make a JexlMap that contains subject and group information and evaluate the given
894    * expression.
895    *
896    * @param expressionName
897    * @param expression
898    * @param subject
899    * @param tsUser
900    * @param grouperGroupInfo
901    * @param tsGroup
902    * @param keysAndValues Key/Value pairs that will also be available within the Jexl's variable map
903    * @return
904    */
905   protected final String evaluateJexlExpression(String expressionName, String expression,
906       Subject subject, TSUserClass tsUser,
907       GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup,
908       Object... keysAndValues) throws PspException {
909     
910     LOG.trace("Evaluating {} Jexl expression: {}", expressionName, expression);
911     
912     Map<String, Object> variableMap = new HashMap<>();
913 
914     variableMap.put("utils", new PspJexlUtils());
915 
916     // Copy any provided keys and values into the map
917     GrouperUtil.assertion(keysAndValues.length % 2 == 0, "KeysAndValues must be paired evenly");
918     for (int i=0; i<keysAndValues.length; i+=2)
919       variableMap.put(keysAndValues[i].toString(), keysAndValues[i+1]);
920 
921     // Give provisioner subclasses to add information
922     populateJexlMap(expression, variableMap, 
923         subject, 
924         tsUser,
925         grouperGroupInfo, 
926         tsGroup);
927     
928     // Give our config a chance to add information
929     config.populateElMap(variableMap);
930     
931     try {
932       // In order to support nested expressions, we're going to repeatedly look for atomic (non-nested) expressions
933       // and replace each atomic ${...} with it's evaluation result until there are no more ${ in the string
934       // This kind of regular expression problem is discussed here:
935       // https://stackoverflow.com/questions/717644/regular-expression-that-doesnt-contain-certain-string
936 
937       Pattern atomicExpressionPattern = Pattern.compile("\\$\\{([^$]|\\$[^{])*?\\}" );
938       String result=expression;
939       Matcher atomicExpressionMatcher = atomicExpressionPattern.matcher(result);
940 
941       while ( atomicExpressionMatcher.find()) {
942         String atomicExpression = atomicExpressionMatcher.group();
943         String atomicExpressionResult;
944 
945         // Check to see if expression has a backup expression
946         //   xyz:-pdq ==> evaluate pdq if xyz cannot be evaluated
947         if ( ! atomicExpression.contains(":-") ) {
948           if (atomicExpression.contains("idIndex") && !variableMap.containsKey("idIndex")) {
949             throw new DeletedGroupException("EL has idIndex but variable doesnt!");
950           }
951           atomicExpressionResult = GrouperUtil.substituteExpressionLanguage(atomicExpression, variableMap, true, false, false);
952         }
953         else {
954           // Split atomicExpression on :- and add } & { to the first and second pieces
955 
956           String expressionOne = StringUtils.substringBefore(atomicExpression, ":-") + "}";
957           String expressionTwo = "${" + StringUtils.substringAfter(atomicExpression, ":-");
958 
959           try {
960             atomicExpressionResult = GrouperUtil.substituteExpressionLanguage(expressionOne, variableMap, true, false, false);
961           } catch (RuntimeException e) {
962             LOG.warn("{}: Problem evaluating '{}'. Will try :- expression '{}': {}",
963                     new Object[]{expressionName, expressionOne, expressionTwo, e.getMessage()});
964 
965 
966             atomicExpressionResult = GrouperUtil.substituteExpressionLanguage(expressionTwo, variableMap, true, false, false);
967           }
968         }
969         LOG.debug("Evaluated {} Jexl expression: '{}'", expressionName, atomicExpressionResult);
970         LOG.trace("Evaluated {} Jexl expression: '{}' FROM {} WITH variables {}",
971                 new Object[]{expressionName, atomicExpressionResult, atomicExpression, variableMap});
972 
973         // replaceFirst unescapes the string it is given (because it isn't a string literal, but can also
974         // refer to regex groupings: $1 refers to the first matching group in the Pattern. In order to get a $ in the
975         // replacement, you need to use \$. We're not using groups in this stuff, but we need to keep \, exactly
976         // as it is; therefore, We need to double-escape atomicExpressionResult... Yuck
977 
978         // This is even more confusing because replaceAll takes a regex and a string literal:
979         //   the first \\\\ is a regex expression for a single \
980         //     (java strings need \\ to make a \ and the regex needs \\ to make a single \)
981         //   and the second \\\\\\\\ (8 whacks) is a substitution string resulting in  two \\
982         result = atomicExpressionMatcher.replaceFirst(atomicExpressionResult.replaceAll("\\\\", "\\\\\\\\"));
983         atomicExpressionMatcher = atomicExpressionPattern.matcher(result);
984       }
985 
986       if ( expression.equals(result) ) {
987         LOG.trace("{} Jexl expression did not include any substitutions: {}", expressionName, expression);
988       } else {
989         LOG.debug("Evaluated entire {} Jexl expression: '{}'", expressionName, result);
990       }
991 
992       return result;
993     }
994     catch (RuntimeException e) {
995       LOG.error("Jexl Expression {} '{}' could not be evaluated for subject '{}/{}' and group '{}/{}' which used variableMap '{}'",
996           new Object[] {expressionName, expression,
997               subject, tsUser,
998               grouperGroupInfo, tsGroup,
999               variableMap, e});
1000       throw new PspException("Jexl evaluation failed: %s", e.getMessage());
1001     }
1002   }
1003 
1004   
1005   /**
1006    * Overridable method to put group and subject information into the Jexl map
1007    * for use during evaluation.
1008    * 
1009    * If you override this, make sure to call super().populateElMap so the standard 
1010    * mappings will be included in addition to those you're adding.
1011    * @param variableMap Map that will eventually be provided in Jexl evalutions
1012    * @param subject
1013    * @param tsUser
1014    * @param grouperGroupInfo
1015    * @param tsGroup
1016    */
1017   protected void populateJexlMap(String expression, Map<String, Object> variableMap, Subject subject, 
1018       TSUserClass tsUser, GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup) {
1019     variableMap.put("provisionerType", getClass().getSimpleName());
1020     variableMap.put("provisionerName", getDisplayName());
1021 
1022     if ( subject != null ) 
1023       variableMap.put("subject", subject);
1024       
1025     if ( tsUser != null )
1026         variableMap.put("tsUser",  tsUser.getJexlMap());
1027     
1028     if ( grouperGroupInfo != null ) {
1029       Map<String, Object> groupMap = getGroupJexlMap(expression, grouperGroupInfo);
1030       variableMap.putAll(groupMap);
1031     }
1032       
1033     if ( tsGroup != null ) 
1034       variableMap.put("tsGroup", tsGroup.getJexlMap());
1035   }
1036 
1037   
1038   
1039   /**
1040    * This makes sure all the Subjects are in tsUserCache_shortTerm. Of
1041    * course if our config says needsTargetSystemUsers is False, then the 
1042    * tsUserCache_shortTerm will always be empty.
1043    * 
1044    * @param subjects
1045    * @throws PspException
1046    */
1047   private void prepareUserCache(Set<Subject> subjects, DateTime oldestCacheTimeAllowed) throws PspException {
1048     LOG.debug("Starting to cache user information for {} items", subjects.size());
1049     tsUserCache_shortTerm.clear();
1050     
1051     // Nothing to do if TargetSystemUsers are not used by this provisioner
1052     if ( ! config.needsTargetSystemUsers() || subjects.size()==0 )
1053       return;
1054     Collection<Subject> subjectsToFetch = new ArrayList<Subject>();
1055     
1056     for (Subject s : subjects) {
1057       // Skip group subjects (source=g:gsa)
1058       if ( s.getSourceId().equals("g:gsa") )
1059         continue;
1060       
1061       // See if the subject is already cached.
1062       TSUserClass cachedTSU = targetSystemUserCache.get(s, oldestCacheTimeAllowed);
1063       if ( cachedTSU != null )
1064         // Cache user in shortTerm cache as well as refresh it in longterm cache
1065         cacheUser(s, cachedTSU);
1066       else
1067         subjectsToFetch.add(s);
1068     }
1069 
1070     LOG.info("{} out of {} subjects were found in cache ({}%)",
1071             tsUserCache_shortTerm.size(), subjects.size(),
1072             (int) 100.0*tsUserCache_shortTerm.size()/subjects.size());
1073 
1074     if ( subjectsToFetch.size() == 0 )
1075       return;
1076 
1077     ProgressMonitoressMonitor.html#ProgressMonitor">ProgressMonitor fetchingProgress = new ProgressMonitor(subjectsToFetch.size(), LOG, false, 15, "Fetching subjects");
1078 
1079     List<List<Subject>> batchesOfSubjectsToFetch = PspUtils.chopped(subjectsToFetch, config.getUserSearch_batchSize());
1080 
1081     List<Future<Map<Subject, TSUserClass>>> futures = new ArrayList<>();
1082 
1083     // Submit these batches to the Fetching ExecutorService and get Futures back
1084     for (final List<Subject> batchOfSubjectsToFetch : batchesOfSubjectsToFetch ) {
1085 
1086       // This essentially calls fetchTargetSystemUsers on the batch and returns
1087       // a Map<Subject, TSUserClass>
1088 
1089       Future<Map<Subject, TSUserClass>> future = tsUserFetchingService.submit(
1090               new Callable<Map<Subject, TSUserClass>>() {
1091                 @Override
1092                 public Map<Subject, TSUserClass> call() throws Exception {
1093                   
1094                   GrouperSession grouperSession = GrouperSession.startRootSession();
1095                   GrouperContext grouperContext = GrouperContext.createNewDefaultContext(GrouperEngineBuiltin.LOADER, false, true);
1096 
1097                   Provisioner.activeProvisioner.set(Provisioner.this);
1098                   Map<Subject, TSUserClass> fetchedData;
1099                   try {
1100                     fetchedData = fetchTargetSystemUsers(batchOfSubjectsToFetch);
1101                   } catch (PspException e1) {
1102                     LOG.warn("Batch-fetching subject information failed. Trying fetching information for each subject individually", e1);
1103                     // Batch-fetching failed. Let's see if we can narrow it down to a single
1104                     // Subject
1105                     fetchedData = new HashMap<>();
1106                     for (Subject subject : batchOfSubjectsToFetch) {
1107                       try {
1108                         TSUserClass tsUser = fetchTargetSystemUser(subject);
1109                         fetchedData.put(subject, tsUser);
1110                       } catch (PspException e2) {
1111                         LOG.error("Problem fetching information about subject '{}'", subject, e2);
1112                         throw new RuntimeException("Problem fetching information on subject " + subject + ": " + e2.getMessage());
1113                       }
1114                     }
1115                   } finally {
1116                     GrouperSession.stopQuietly(grouperSession);
1117                     GrouperContext.deleteDefaultContext();
1118                   }
1119                   return fetchedData;
1120                 }
1121               });
1122 
1123       futures.add(future);
1124     }
1125 
1126     // Gather up the futures until there are no more
1127     while (!futures.isEmpty()) {
1128       Iterator<Future<Map<Subject, TSUserClass>>> futureIterator = futures.iterator();
1129 
1130       while (futureIterator.hasNext()) {
1131         Future<Map<Subject, TSUserClass>> future = futureIterator.next();
1132 
1133         // Process (and remove) any future that has completed its fetching
1134         if ( future.isDone() ) {
1135           Map<Subject, TSUserClass> fetchedData;
1136           try {
1137             fetchedData = future.get();
1138           } catch (InterruptedException e) {
1139             LOG.error("Problem fetching information on subjects", e);
1140 
1141             throw new RuntimeException("Problem fetching information on subjects: " + e.getMessage());
1142           } catch (ExecutionException e) {
1143             LOG.error("Problem fetching information on subjects", e);
1144 
1145             throw new RuntimeException("Problem fetching information on subjects: " + e.getMessage());
1146           }
1147 
1148           // Save the fetched data in our cache
1149           for ( Entry<Subject, TSUserClass> subjectInfo : fetchedData.entrySet() )
1150             cacheUser(subjectInfo.getKey(), subjectInfo.getValue());
1151 
1152           fetchingProgress.workCompleted(fetchedData.size());
1153           futureIterator.remove();
1154         }
1155       }
1156     }
1157     fetchingProgress.completelyDone("Success");
1158 
1159     // CREATE MISSING TARGET SYSTEM USERS (IF ENABLED)
1160     // Go through the subjects and see if any of them were not found above. 
1161     // If user-creation is enabled, just create the
1162     // user in the target system and add it to our caches
1163     for ( Subject subj : subjects ) {
1164       if ( !tsUserCache_shortTerm.containsKey(subj) ) {
1165         if ( config.isCreatingMissingUsersEnabled() ) {
1166           TSUserClass newTSUser = createUser(subj);
1167           if ( newTSUser != null )
1168             cacheUser(subj, newTSUser);
1169         }
1170         else
1171           LOG.warn("{}: User not found in target system: {}", getDisplayName(), subj.getId());
1172       }
1173     }
1174   }
1175 
1176  
1177   /**
1178    * This makes sure all the Groups referenced by groupInfoSet are in groupMap_shortTerm. 
1179    * If our config says needsTargetSystemGroups is False, then the groupMap will
1180    * be empty.
1181    *
1182    * Note: This will create missing groups
1183    * 
1184    * @param grouperGroupInfos
1185    * @throws PspException
1186    */
1187   private void prepareGroupCache(Collection<GrouperGroupInfo> grouperGroupInfos) throws PspException {
1188 	// Remove any duplicate group info objects
1189 	Set<GrouperGroupInfo> groupInfoSet = new HashSet<>(grouperGroupInfos);
1190 	
1191     LOG.debug("Starting to cache group information for {} items", groupInfoSet.size());
1192     tsGroupCache_shortTerm.clear();
1193     
1194     // If the target system doesn't need groups, then we obviously don't need to 
1195     // fetch and cache them
1196     if ( ! config.needsTargetSystemGroups() )
1197       return;
1198     
1199     Collection<GrouperGroupInfo> groupsToFetch = new ArrayList<GrouperGroupInfo>();
1200 
1201     for (GrouperGroupInfo grouperGroupInfo : groupInfoSet) {
1202       // See if the group is already cached.
1203       TSGroupClass cachedTSG = targetSystemGroupCache.get(grouperGroupInfo);
1204       if (cachedTSG != null) {
1205         // Cache group in shortTerm cache as well as refresh it in longterm cache
1206         cacheGroup(grouperGroupInfo, cachedTSG);
1207       }
1208       else {
1209         groupsToFetch.add(grouperGroupInfo);
1210       }
1211     }
1212 
1213     Map<GrouperGroupInfo, TSGroupClass> fetchedData = fetchTargetSystemGroupsInBatches(groupsToFetch);
1214     // Save the data that was fetched in our cache
1215     for ( Entry<GrouperGroupInfo, TSGroupClass> grouperGroupInfo : fetchedData.entrySet() )
1216       cacheGroup(grouperGroupInfo.getKey(), grouperGroupInfo.getValue());
1217 
1218     // If empty groups are supported, then look for groups that were not found in target
1219     // system and create them
1220     //
1221     // Note: If empty groups are not supported, they will be created later... when their
1222     // first membership is provisioned
1223     if ( config.areEmptyGroupsSupported() ) {
1224       for (GrouperGroupInfo grouperGroupInfo : groupsToFetch) {
1225         if (!tsGroupCache_shortTerm.containsKey(grouperGroupInfo) &&
1226             shouldGroupBeProvisionedConsiderCache(grouperGroupInfo))
1227         {
1228           // Group does not already exist so create it
1229           TSGroupClass tsGroup = createGroup(grouperGroupInfo, new ArrayList<Subject>());
1230           cacheGroup(grouperGroupInfo, tsGroup);
1231         }
1232       }
1233     }
1234 
1235   }
1236 
1237   /**
1238    * This method fetches an arbitrary number of groups from the target system. The configuration
1239    * of the system defines a maximum-batch-fetch size (config.getGroupSearch_batchSize()), and
1240    * this method breaks the given groups into appropriately-sized batches.
1241    *
1242    * @param groupsToFetch
1243    * @throws PspException
1244    */
1245   public Map<GrouperGroupInfo, TSGroupClass> fetchTargetSystemGroupsInBatches(Collection<GrouperGroupInfo> groupsToFetch) throws PspException {
1246     Map<GrouperGroupInfo, TSGroupClass> result = new HashMap<>(groupsToFetch.size());
1247 
1248     List<List<GrouperGroupInfo>> batchesOfGroupsToFetch = PspUtils.chopped(groupsToFetch, config.getGroupSearch_batchSize());
1249 
1250     for ( List<GrouperGroupInfo> batchOfGroupsToFetch : batchesOfGroupsToFetch ) {
1251       Map<GrouperGroupInfo, TSGroupClass> fetchedData;
1252 
1253       try {
1254         fetchedData = fetchTargetSystemGroups(batchOfGroupsToFetch);
1255         result.putAll(fetchedData);
1256       }
1257       catch (PspException e1) {
1258         LOG.warn("Batch-fetching group information failed. Trying to fetch information for each group individually", e1);
1259         // Batch-fetching failed. Let's see if we can narrow it down to a single
1260         // Group
1261           for ( GrouperGroupInfo grouperGroupInfo : batchOfGroupsToFetch ) {
1262             try {
1263               TSGroupClass tsGroup = fetchTargetSystemGroup(grouperGroupInfo);
1264               cacheGroup(grouperGroupInfo, tsGroup);
1265             }
1266             catch (PspException e2) {
1267               LOG.error("Problem fetching information on group '{}'", grouperGroupInfo, e2);
1268               throw new RuntimeException("Problem fetching information on group " + grouperGroupInfo);
1269             }
1270           }
1271       }
1272     }
1273 
1274     return result;
1275   }
1276 
1277 
1278   public TSUserClass getTargetSystemUser(Subject subject) throws PspException {
1279     GrouperUtil.assertion(config.needsTargetSystemUsers(),
1280             String.format("%s: system doesn't need target-system users, but one was requested", getDisplayName()));
1281     
1282     TSUserClass result = tsUserCache_shortTerm.get(subject);
1283     
1284     if ( result == null ) {
1285       if ( config.isCreatingMissingUsersEnabled() ) {
1286         result=createUser(subject);
1287         cacheUser(subject, result);
1288       }
1289       else {
1290         LOG.warn("{}: user is missing and user-creation is not enabled ({})", getDisplayName(), subject.getId());
1291       }
1292     }
1293     
1294     return result;
1295   }
1296   
1297   
1298   /**
1299    * Store Subject-->TSUserClass mapping in long-term and short-term caches
1300    * @param subject
1301    * @param newTSUser
1302    */
1303   private void cacheUser(Subject subject, TSUserClass newTSUser) {
1304     LOG.debug("Adding target-system user to cache: {}", subject);
1305     targetSystemUserCache.put(subject, newTSUser);
1306     tsUserCache_shortTerm.put(subject, newTSUser);
1307   }
1308 
1309 
1310   /**
1311    * Store Group-->TSGroupClass mapping in long-term and short-term caches
1312    * @param grouperGroupInfo
1313    * @param newTSGroup
1314    */
1315   protected void cacheGroup(GrouperGroupInfo grouperGroupInfo, TSGroupClass newTSGroup) {
1316     if ( newTSGroup != null ) {
1317       LOG.debug("Adding target-system group to cache: {}", grouperGroupInfo);
1318       targetSystemGroupCache.put(grouperGroupInfo, newTSGroup);
1319       tsGroupCache_shortTerm.put(grouperGroupInfo, newTSGroup);
1320     } else {
1321       if ( targetSystemGroupCache.containsKey(grouperGroupInfo) ||
1322            tsGroupCache_shortTerm.containsKey(grouperGroupInfo) ) {
1323         LOG.debug("Removing target-system group from cache: {}", grouperGroupInfo);
1324         targetSystemGroupCache.remove(grouperGroupInfo);
1325         tsGroupCache_shortTerm.remove(grouperGroupInfo);
1326       } else {
1327         LOG.debug("No target-system group to cache: {}", grouperGroupInfo);
1328       }
1329     }
1330   }
1331   
1332   
1333   /**
1334    * The specified Grouper or TargetSystem group has changed, remove
1335    * them from various caches, including hibernate L2 cache.
1336    *
1337    * Only one parameter is needed
1338    * @param grouperGroupInfo
1339    * @param tsGroup
1340    */
1341   protected void  uncacheGroup(GrouperGroupInfo grouperGroupInfo, TSGroupClass tsGroup) {
1342     // If the caller only knew the TargetSystem Group and didn't know what Grouper Group to flush,
1343     // let's see if we can find it
1344     if ( grouperGroupInfo == null && tsGroup != null ) {
1345       for (GrouperGroupInfo gi : targetSystemGroupCache.keySet())
1346         if (targetSystemGroupCache.get(gi) == tsGroup) {
1347           grouperGroupInfo = gi;
1348           break;
1349         }
1350 
1351       if ( grouperGroupInfo == null ) {
1352         LOG.warn("Can't find Grouper Group to uncache from tsGroup {}", tsGroup);
1353         return;
1354       }
1355     }
1356 
1357     LOG.debug("Flushing group from target-system cache: {}", grouperGroupInfo);
1358     targetSystemGroupCache.remove(grouperGroupInfo);
1359 
1360     LOG.debug("Flushing group from pspng group-info cache: {}", grouperGroupInfo.getName());
1361     grouperGroupInfoCache.remove(grouperGroupInfo.getName());
1362 
1363     grouperGroupInfo.hibernateRefresh();
1364   }
1365 
1366 
1367   /**
1368    * This removes all Group information from our caches
1369    */
1370   protected void uncacheAllGroups() {
1371 
1372     for (GrouperGroupInfo g : grouperGroupInfoCache.values()) {
1373       uncacheGroup(g, null);
1374     }
1375   }
1376 
1377 
1378   /**
1379    * Lookup a single TSUserClass for a single Subject. If you have several such mappings to look up,
1380    * you should use the (plural version) fetchTargetSystemUsers( ) instead, as that will have an opportunity 
1381    * to do faster batch fetching.
1382    * 
1383    * Note: This is final. Systems that have/need TargetSystemUsers need to override fetchTargetSystemUsers.
1384    * 
1385    * @param personSubject
1386    * @return
1387    * @throws PspException
1388    */
1389   protected final TSUserClass fetchTargetSystemUser(Subject personSubject) throws PspException {
1390     // Forward this singluar subject to the multi-subject version.
1391     Map<Subject, TSUserClass> result = fetchTargetSystemUsers(Arrays.asList(personSubject));
1392     return result.get(personSubject);
1393   }
1394   
1395   /**
1396    * Lookup a single TSGroupClass for a single (grouper) Group. If you have several such mappings to look up,
1397    * you should use the (plural version) fetchTargetSystemGroups( ) instead, as that will have an opportunity to do 
1398    * faster batch fetching.
1399    * 
1400    * Note: This is final. Systems that have/need TargetSystemGroups need to override fetchTargetSystemGroups.
1401    * 
1402    * @param grouperGroup
1403    * @return
1404    * @throws PspException
1405    */
1406   protected final TSGroupClass fetchTargetSystemGroup(GrouperGroupInfo grouperGroup) throws PspException {
1407     // Forward this singluar Group to the multi-subject version.
1408     Map<GrouperGroupInfo, TSGroupClass> result = fetchTargetSystemGroups(Arrays.asList(grouperGroup));
1409     return result.get(grouperGroup);
1410   }
1411   
1412   /**
1413    * Provisioning a new User account in the target system. This must be overridden in provisioner
1414    * subclasses that support creating user accounts. This is more used for provisioning testing, but
1415    * can be supported in production when provisioning can be done with just the information stored
1416    * in a grouper Subject.
1417    * 
1418    * @param personSubject
1419    * @return
1420    * @throws PspException
1421    */
1422   protected TSUserClass createUser(Subject personSubject) throws PspException {
1423     return null;
1424   }
1425   
1426   /**
1427    * Dispatches an event to the right method, with generally
1428    * useful parameters. 
1429    * 
1430    * There is no need to override this method when implementing basic provisioning 
1431    * (add/delete groups and members), but a Provisioner subclasses might override this 
1432    * to capture other CategoryAndActions combinations. 
1433    * 
1434    * @param workItem
1435    */
1436   protected void provisionItem(ProvisioningWorkItem workItem) throws PspException {
1437     LOG.debug("Starting provisioning of item: {}", workItem);
1438     
1439     currentWorkItem.set(workItem);
1440     ChangeLogEntry entry = workItem.getChangelogEntry();
1441     String workItem_identifier = null;
1442     if ( workItem.getChangelogEntry()!=null ) {
1443       workItem_identifier = Long.toString(workItem.getChangelogEntry().getSequenceNumber());
1444     }
1445 
1446     try {
1447       if ( workItem.matchesChangelogType(ChangelogHandlingConfig.changelogTypesThatAreHandledIncrementally) ) {
1448         processIncrementalSyncEvent(workItem);
1449         return;
1450       }
1451 
1452       if ( workItemMightChangeGroupSelection(workItem) ) {
1453         processAnyChangesInGroupSelection(workItem);
1454       }
1455 
1456       // If this is a specific group and is still selected for provisioning,
1457       // then we need to do a full sync of that group in case an attribute or
1458       // another non-membership aspect of the group changed
1459       if ( workItem.getGroupInfo(this) != null &&
1460                 selectedGroups.get().contains(workItem.getGroupInfo(this)) ) {
1461         // This is a changelog entry that modifies the group. Do a FullSync to see if any
1462         // provisioned information changed. Unfortunately, this will do a membership sync which
1463         // might slow down the processing of this changelog entry. However, non-membership
1464         // changes are expected to be infrequent, so we aren't creating an optimized code path
1465         // that doesn't sync memberships.
1466 
1467         FullSyncQueueItem fullSyncStatus = getFullSyncer()
1468                 .scheduleGroupForSync(
1469                         FullSyncProvisioner.QUEUE_TYPE.ASAP,
1470                         workItem.groupName,
1471                         workItem_identifier,
1472                         "Changelog: %s", workItem);
1473 
1474         workItem.markAsSuccess("Handled with scheduled FullSync (qid=%d)", fullSyncStatus.id);
1475         return;
1476       }
1477 
1478       // If we've gotten this far without processing it, there's nothing we can do
1479       if ( !workItem.hasBeenProcessed() ) {
1480         workItem.markAsSkipped("Nothing to do (not a supported change)");
1481       }
1482 
1483     } catch (PspException e) {
1484       LOG.error("Problem provisioning item {}", workItem, e);
1485       workItem.markAsFailure("Provisioning failure: %s", e.getMessage());
1486     } finally {
1487       currentWorkItem.set(null);
1488     }
1489   }
1490 
1491   private void processAnyChangesInGroupSelection(ProvisioningWorkItem workItem) throws PspException {
1492     String workItem_identifier;
1493     ChangeLogEntry changelogEntry = workItem.getChangelogEntry();
1494     if ( changelogEntry != null ) {
1495       workItem_identifier = String.format("chlog #%d",changelogEntry.getSequenceNumber());
1496     } else {
1497       workItem_identifier = workItem.toString();
1498     }
1499 
1500     LOG.info("{}: Checking to see if group selection has changed", getDisplayName());
1501     Set<GrouperGroupInfo> selectedGroups_before = selectedGroups.get();
1502     Set<GrouperGroupInfo> selectedGroups_now = getAllGroupsForProvisioner();
1503 
1504     // deselectedGroups = BEFORE GROUPS -minus- NOW GROUPS
1505     Set<GrouperGroupInfo> deselectedGroups = new HashSet<>(selectedGroups_before);
1506     deselectedGroups.removeAll(selectedGroups_now);
1507 
1508     // newlySelectedGroups = NOW GROUPS -minus- BEFORE GROUPS
1509     Set<GrouperGroupInfo> newlySelectedGroups = new HashSet<>(selectedGroups_now);
1510     newlySelectedGroups.removeAll(selectedGroups_before);
1511 
1512     // Save updated list of groups selected for provisioner
1513     selectedGroups.set(selectedGroups_now);
1514 
1515     LOG.info("{}: Change deselected {} groups from provisioner", getDisplayName(), deselectedGroups.size());
1516     LOG.info("{}: Change selected {} new groups for provisioner", getDisplayName(), newlySelectedGroups.size());
1517 
1518     for (GrouperGroupInfo group : deselectedGroups) {
1519       TSGroupClass tsGroup=null;
1520       if ( getConfig().needsTargetSystemGroups() ) {
1521         tsGroup = fetchTargetSystemGroup(group);
1522         if ( tsGroup == null ) {
1523           LOG.info("{}: Group is already not present in target system: {}", getDisplayName(), group.getName());
1524           continue;
1525         }
1526       }
1527       LOG.info("{}: Deleting group from target system because it is no longer selected for provisioning: {}", getDisplayName(), group);
1528       deleteGroup(group, tsGroup);
1529     }
1530 
1531     for (GrouperGroupInfo group : newlySelectedGroups) {
1532       LOG.info("{}: Scheduling full sync of group because it is now selected for provisioning: {}", getDisplayName(), group);
1533 
1534       JobStatistics jobStatistics = this.getJobStatistics();
1535       if (jobStatistics != null) {
1536         jobStatistics.insertCount.addAndGet(1);
1537       }
1538 
1539       getFullSyncer().scheduleGroupForSync(
1540               FullSyncProvisioner.QUEUE_TYPE.CHANGELOG,
1541               group.getName(),
1542               workItem_identifier,
1543               "group newly selected for provisioning");
1544 
1545     }
1546 
1547     String attributeValue = null;
1548     String attributeAssignId = null;
1549     String nameOfAttributeDefName = null;
1550     boolean isAddAndNotDelete = false;
1551     
1552     // if this is an add value or delete value, and the value matches, then process that group(s) or folder(s)
1553     if (changelogEntry.equalsCategoryAndAction(ChangeLogTypeBuiltin.ATTRIBUTE_ASSIGN_VALUE_ADD)) {
1554       attributeValue = changelogEntry.retrieveValueForLabel(ChangeLogLabels.ATTRIBUTE_ASSIGN_VALUE_ADD.value);
1555       attributeAssignId = changelogEntry.retrieveValueForLabel(ChangeLogLabels.ATTRIBUTE_ASSIGN_VALUE_ADD.attributeAssignId);
1556       nameOfAttributeDefName = changelogEntry.retrieveValueForLabel(ChangeLogLabels.ATTRIBUTE_ASSIGN_VALUE_ADD.attributeDefNameName);
1557       isAddAndNotDelete = true;
1558     }
1559 
1560     if (changelogEntry.equalsCategoryAndAction(ChangeLogTypeBuiltin.ATTRIBUTE_ASSIGN_VALUE_DELETE)) {
1561       attributeValue = changelogEntry.retrieveValueForLabel(ChangeLogLabels.ATTRIBUTE_ASSIGN_VALUE_DELETE.value);
1562       attributeAssignId = changelogEntry.retrieveValueForLabel(ChangeLogLabels.ATTRIBUTE_ASSIGN_VALUE_DELETE.attributeAssignId);
1563       nameOfAttributeDefName = changelogEntry.retrieveValueForLabel(ChangeLogLabels.ATTRIBUTE_ASSIGN_VALUE_DELETE.attributeDefNameName);
1564     }
1565 
1566     String provisionToName = GrouperConfig.retrieveConfig().propertyValueString(
1567         "grouper.rootStemForBuiltinObjects", "etc") + ":pspng:provision_to";
1568     String doNotProvisionToName = GrouperConfig.retrieveConfig().propertyValueString(
1569         "grouper.rootStemForBuiltinObjects", "etc") + ":pspng:do_not_provision_to";
1570 
1571     // attribute value affects this provisioner
1572     if (!StringUtils.isBlank(attributeAssignId) && !StringUtils.isBlank(attributeValue) && StringUtils.equals(attributeValue, this.provisionerConfigName)
1573         && getConfig().getAttributesUsedInGroupSelectionExpression().contains(nameOfAttributeDefName)
1574         && (StringUtils.equals(provisionToName, nameOfAttributeDefName) || StringUtils.equals(doNotProvisionToName, nameOfAttributeDefName))) {
1575       
1576       List<String> groupNames = new GcDbAccess().sql("SELECT gg.name FROM grouper_attribute_assign gaa, grouper_groups gg "
1577           + "WHERE gaa.id = ? AND gg.id = gaa.OWNER_GROUP_ID and gg.name is not null").addBindVar(attributeAssignId).selectList(String.class);
1578 
1579       if (GrouperUtil.length(groupNames) == 0) {
1580         groupNames = new GcDbAccess().sql("SELECT gg.name FROM grouper_attribute_assign gaa, grouper_stems gs, grouper_stem_set gss, grouper_groups gg "
1581             + " WHERE gss.then_has_stem_id = gs.id AND gg.PARENT_STEM  = gss.IF_HAS_STEM_ID "   
1582             + " AND gaa.id = ? AND gs.id = gaa.OWNER_STEM_ID").addBindVar(attributeAssignId).selectList(String.class);
1583       }        
1584 
1585       if (GrouperUtil.length(groupNames) == 0) {
1586         groupNames = new GcDbAccess().sql("SELECT gpg.name FROM grouper_pit_attribute_assign gpaa, grouper_pit_groups gpg "
1587             + "WHERE gpaa.source_id = ? AND gpg.id = gpaa.OWNER_GROUP_ID AND gpaa.END_TIME IS NULL and gpg.name is not null")
1588             .addBindVar(attributeAssignId).selectList(String.class);
1589       }
1590 
1591       if (GrouperUtil.length(groupNames) == 0) {
1592         groupNames = new GcDbAccess().sql("SELECT gpg.name FROM grouper_pit_attribute_assign gpaa, grouper_pit_groups gpg "
1593             + "WHERE gpaa.source_id = ? AND gpg.name IS NOT NULL AND gpg.id = gpaa.OWNER_GROUP_ID AND gpaa.END_TIME = "
1594             + "(SELECT max(gpaa2.end_time) FROM grouper_pit_attribute_assign gpaa2 WHERE gpaa2.end_time IS NOT NULL AND gpaa.source_id = gpaa2.source_id)")
1595             .addBindVar(attributeAssignId).selectList(String.class);
1596       }
1597 
1598       if (GrouperUtil.length(groupNames) == 0) {
1599         groupNames = new GcDbAccess().sql("SELECT gg.name FROM grouper_pit_attribute_assign gpaa, grouper_pit_stems gps, "
1600             + "grouper_stem_set gss, grouper_groups gg "
1601             + "WHERE gss.then_has_stem_id = gps.source_id AND gg.PARENT_STEM  = gss.IF_HAS_STEM_ID AND gpaa.end_time IS null "
1602             + "AND gpaa.source_id = ? AND gps.id = gpaa.OWNER_STEM_ID")
1603             .addBindVar(attributeAssignId).selectList(String.class);
1604       }
1605 
1606       if (GrouperUtil.length(groupNames) == 0) {
1607         groupNames = new GcDbAccess().sql("SELECT gg.name FROM grouper_pit_attribute_assign gpaa, grouper_pit_stems gps, grouper_stem_set gss, grouper_groups gg "
1608             + "WHERE gss.then_has_stem_id = gps.source_id AND gg.PARENT_STEM  = gss.IF_HAS_STEM_ID "
1609             + "AND gpaa.source_id = ? AND gps.id = gpaa.OWNER_STEM_ID "
1610             + "AND gpaa.end_time = (SELECT max(gpaa2.end_time) FROM grouper_pit_attribute_assign gpaa2 WHERE gpaa2.end_time IS NOT NULL AND gpaa.source_id = gpaa2.source_id)")
1611             .addBindVar(attributeAssignId).selectList(String.class);
1612       }
1613       
1614 
1615 //    String stemName = new GcDbAccess().sql("SELECT gs.name FROM grouper_attribute_assign gaa, grouper_stems gs "
1616 //    + "WHERE gaa.id = ? AND gs.id = gaa.OWNER_STEM_ID").addBindVar(attributeAssignId).select(String.class);
1617 
1618   // if groups are deleted they will be handled elsewhere
1619 //  groupName = new GcDbAccess().sql("SELECT gpg.NAME FROM grouper_pit_attribute_assign gpaa, grouper_pit_groups gpg "
1620 //      + "WHERE gpaa.source_id = ? AND gpaa.owner_group_id = gpg.id and gpaa.END_TIME IS null").addBindVar(attributeAssignId).select(String.class);
1621 //  stemName = new GcDbAccess().sql("SELECT gps.NAME FROM grouper_pit_attribute_assign gpaa, grouper_pit_stems gps "
1622 //      + "WHERE gpaa.source_id = ? AND gpaa.owner_stem_id = gps.id and gpaa.END_TIME IS null").addBindVar(attributeAssignId).select(String.class);
1623 //  
1624 ////  groupName = new GcDbAccess().sql("SELECT gpg.NAME FROM grouper_pit_groups gpg WHERE source_id = ? and gpg.END_TIME IS null")
1625 ////      .addBindVar(attributeAssign.getOwnerGroupId()).select(String.class);
1626 ////
1627 ////  groupName = new GcDbAccess().sql("SELECT gpg.NAME FROM grouper_pit_groups gpg WHERE source_id = ? and gpg.END_TIME = "
1628 ////      + "(SELECT max(gpg2.end_time) FROM grouper_pit_groups gpg2 WHERE gpg2.source_id = gpg.source_id AND gpg2.end_time IS NOT null)")
1629 ////         .addBindVar(attributeAssign.getOwnerGroupId()).select(String.class);
1630 
1631 
1632       HashSet<String> groupNameSet = new HashSet<String>(GrouperUtil.nonNull(groupNames));
1633       for (String groupName : groupNameSet) {
1634 
1635         // if true, this is adding provisionable or removing do_not_provision, else removing pro
1636         boolean newlyProvisionable = (StringUtils.equals(provisionToName, nameOfAttributeDefName) && isAddAndNotDelete) 
1637             || (StringUtils.equals(doNotProvisionToName, nameOfAttributeDefName) && !isAddAndNotDelete);
1638         
1639         GrouperGroupInfo group = getGroupInfoOfExistingGroup(groupName);
1640 
1641         boolean provisionable = GrouperUtil.nonNull(selectedGroups_now).contains(group);
1642         
1643         if (!newlyProvisionable) {
1644           if (!provisionable) {
1645             TSGroupClass tsGroup=null;
1646             if ( getConfig().needsTargetSystemGroups() ) {
1647               tsGroup = fetchTargetSystemGroup(group);
1648               if ( tsGroup == null ) {
1649                 LOG.info("{}: Group is already not present in target system: {}", getDisplayName(), group == null ? "null" : group.getName());
1650                 continue;
1651               }
1652             }
1653             LOG.info("{}: Deleting group from target system because it is no longer selected for provisioning: {}", getDisplayName(), group);
1654             deleteGroup(group, tsGroup);
1655           }
1656         } else {
1657           if (provisionable) {
1658             LOG.info("{}: Scheduling full sync of group because it is now selected for provisioning: {}", getDisplayName(), group);
1659   
1660             JobStatistics jobStatistics = this.getJobStatistics();
1661             if (jobStatistics != null) {
1662               jobStatistics.insertCount.addAndGet(1);
1663             }
1664 
1665             getFullSyncer().scheduleGroupForSync(
1666                     FullSyncProvisioner.QUEUE_TYPE.CHANGELOG,
1667                     group.getName(),
1668                     workItem_identifier,
1669                     "group newly selected for provisioning");
1670             
1671           }
1672         }
1673 
1674       
1675       }
1676       
1677       
1678     }
1679     
1680     workItem.markAsSuccess("Processed any changes in group selection");
1681   }
1682 
1683 
1684   private void processIncrementalSyncEvent(ProvisioningWorkItem workItem) throws PspException {
1685     ChangeLogEntry entry = workItem.getChangelogEntry();
1686 
1687     if ( entry.equalsCategoryAndAction(ChangeLogTypeBuiltin.GROUP_ADD ))
1688     {
1689       GrouperGroupInfo grouperGroupInfo = workItem.getGroupInfo(this);
1690 
1691       if ( grouperGroupInfo == null || grouperGroupInfo.hasGroupBeenDeleted() ) {
1692         workItem.markAsSkippedAndWarn("Ignored group-add: group does not exist any more");
1693         return;
1694       }
1695 
1696       if ( !shouldGroupBeProvisionedConsiderCache(grouperGroupInfo) ) {
1697         workItem.markAsSkipped("Group %s is not selected to be provisioned", grouperGroupInfo);
1698         return;
1699       } else {
1700         // Document that this new group should be provisioned.
1701         selectedGroups.get().add(grouperGroupInfo);
1702       }
1703 
1704       if ( tsGroupCache_shortTerm.containsKey(grouperGroupInfo) ) {
1705         workItem.markAsSuccess("Group %s already exists", grouperGroupInfo);
1706         return;
1707       }
1708       else
1709         createGroup(grouperGroupInfo, Collections.EMPTY_LIST);
1710     }
1711     else if ( entry.equalsCategoryAndAction(ChangeLogTypeBuiltin.GROUP_DELETE ))
1712     {
1713       // Can't tell right reliably if group was supposed to be provisioned when it existed, so
1714       // we just delete it if it currently exists in target
1715       GrouperGroupInfo grouperGroupInfo = workItem.getGroupInfo(this);
1716 
1717       if ( grouperGroupInfo == null ) {
1718         workItem.markAsSkippedAndWarn("Ignoring group-deletion event because group information was not found in grouper");
1719         return;
1720       }
1721 
1722       TSGroupClass tsGroup = tsGroupCache_shortTerm.get(grouperGroupInfo);
1723 
1724       // If system uses targetSystemGroup and one doesn't exist ==> Nothing to do
1725       if ( config.needsTargetSystemGroups() ) {
1726           if ( tsGroup == null ) {
1727               workItem.markAsSuccess("Nothing to do: Group does not exist in target system");
1728               return;
1729           } else {
1730               LOG.info("Deleting provisioned group: {}", grouperGroupInfo);
1731               deleteGroup(grouperGroupInfo, tsGroup);
1732           }
1733       }
1734       else {
1735         // System doesn't use targetSystemGroup, so we can't tell if this group
1736         // was provisioned or not. Therefore call deleteGroup just in case
1737 
1738         LOG.info("{}: Group has been deleted from grouper. Checking to see if it was provisioned to target system", getDisplayName());
1739         deleteGroup(grouperGroupInfo, tsGroup);
1740       }
1741     }
1742     else if ( entry.equalsCategoryAndAction(ChangeLogTypeBuiltin.MEMBERSHIP_ADD))
1743     {
1744       GrouperGroupInfo grouperGroupInfo = workItem.getGroupInfo(this);
1745 
1746       if ( grouperGroupInfo == null || grouperGroupInfo.hasGroupBeenDeleted() ) {
1747         workItem.markAsSkipped("Ignoring membership-add event for group that was deleted");
1748         return;
1749       }
1750 
1751       if ( !shouldGroupBeProvisioned(grouperGroupInfo) ) {
1752         workItem.markAsSkipped("Group %s is not selected to be provisioned", grouperGroupInfo);
1753         return;
1754       }
1755 
1756       TSGroupClass tsGroup = tsGroupCache_shortTerm.get(grouperGroupInfo);
1757       Subject subject = workItem.getSubject(this);
1758 
1759       if ( subject == null ) {
1760         workItem.markAsSkippedAndWarn("Ignoring membership-add event because subject is no longer in grouper");
1761         return;
1762       }
1763 
1764       if ( subject.getTypeName().equalsIgnoreCase("group") ) {
1765         workItem.markAsSkipped("Nested-group membership skipped");
1766         return;
1767       }
1768 
1769       TSUserClass tsUser = tsUserCache_shortTerm.get(subject);
1770 
1771       if ( config.needsTargetSystemUsers() && tsUser==null ) {
1772         workItem.markAsSkippedAndWarn("Skipped: subject doesn't exist in target system");
1773         return;
1774       }
1775 
1776       addMembership(grouperGroupInfo, tsGroup, subject, tsUser);
1777     }
1778     else if ( entry.equalsCategoryAndAction(ChangeLogTypeBuiltin.MEMBERSHIP_DELETE))
1779     {
1780       GrouperGroupInfo grouperGroupInfo = workItem.getGroupInfo(this);
1781 
1782       if ( grouperGroupInfo==null || grouperGroupInfo.hasGroupBeenDeleted() ) {
1783         workItem.markAsSkipped("Ignoring membership-delete event for group that was deleted");
1784         return;
1785       }
1786 
1787       if ( !shouldGroupBeProvisioned(grouperGroupInfo) ) {
1788         workItem.markAsSkipped("Group %s is not selected to be provisioned", grouperGroupInfo);
1789         return;
1790       }
1791 
1792       TSGroupClass tsGroup = tsGroupCache_shortTerm.get(grouperGroupInfo);
1793       Subject subject = workItem.getSubject(this);
1794 
1795       if ( subject == null ) {
1796         workItem.markAsSkippedAndWarn("Ignoring membership-delete event because subject is no longer in grouper");
1797         LOG.warn("Work item ignored: {}", workItem);
1798         return;
1799       }
1800 
1801       TSUserClass tsUser = tsUserCache_shortTerm.get(subject);
1802 
1803       if ( config.needsTargetSystemUsers() && tsUser==null ) {
1804         workItem.markAsSkippedAndWarn("Skipped: subject doesn't exist in target system");
1805         return;
1806       }
1807       deleteMembership(grouperGroupInfo, tsGroup, subject, tsUser);
1808     }
1809   }
1810 
1811 
1812   /**
1813    * Method that drives the cleanup operation. This sets things up and then
1814    * calls the (Abstract) doFullSync_cleanupExtraGroups
1815    *
1816    * @param stats
1817    * @throws PspException
1818    */
1819   final void prepareAndRunGroupCleanup(JobStatistics stats) throws PspException {
1820       // Make sure this is only used within Provisioners set up for full-sync mode
1821       GrouperUtil.assertion(isFullSyncMode(), "FullSync operations should only be used with provisioners initialized for full-sync");
1822 
1823 	  if ( !config.isGrouperAuthoritative() ) {
1824 		  LOG.warn("{}: Not doing group cleanup because grouper is not marked as authoritative in provisioner configuration", getDisplayName());
1825 		  return;
1826 	  }
1827 	  
1828 	  tsUserCache_shortTerm.clear();
1829 	  tsGroupCache_shortTerm.clear();
1830 	  try {
1831 	    MDC.put("step", "clean/");
1832 	    doFullSync_cleanupExtraGroups(stats);
1833 	  }
1834 	  catch (PspException e) {
1835 		  LOG.error("Problem while looking for and removing extra groups", e);
1836 		  throw e;
1837 	  }
1838 	  finally {
1839 		  MDC.remove("step");
1840 	  }
1841   }
1842   
1843 
1844 /**
1845    * This is called by the FullSync thread, and is responsible for getting the
1846    * list of correct subjects together and cached. 
1847    * 
1848    * This then calls (abstract) doFullSync(group, tsGroup, correctSubjects, correctTSUsers, stats).
1849    * 
1850    * This method is final to make it clear that the abstract signature should be
1851    * overridden and to prevent subclasses from overriding this one by mistake.
1852    * 
1853    * @param grouperGroupInfo
1854  * @param oldestCacheTimeAllowed only use cached information fresher than this date
1855  * @param stats
1856  * @return True when changes were made to target system
1857  @throws PspException
1858    */
1859   final boolean doFullSync(GrouperGroupInfo grouperGroupInfo, DateTime oldestCacheTimeAllowed, JobStatistics stats)
1860         throws PspException {
1861     // Make sure this is only used within Provisioners set up for full-sync mode
1862     GrouperUtil.assertion(isFullSyncMode(),
1863             "FullSync operations should only be used with provisioners initialized for full-sync");
1864 
1865 
1866     TSGroupClass tsGroup = tsGroupCache_shortTerm.get(grouperGroupInfo);
1867 
1868     // If there is a target-system group, then check to see if it should be deleted
1869     if ( tsGroup != null ) {
1870       if (grouperGroupInfo.hasGroupBeenDeleted() && tsGroup != null) {
1871         LOG.info("{} full sync: Deleting group because it was deleted from grouper: {}/{}",
1872                 new Object[]{getDisplayName(), grouperGroupInfo, tsGroup});
1873 
1874         deleteGroup(grouperGroupInfo, tsGroup);
1875         return true;
1876       }
1877 
1878       if (!shouldGroupBeProvisionedConsiderCache(grouperGroupInfo)) {
1879         LOG.info("{} full sync: Deleting group because it is not selected for this provisioner: {}/{}",
1880                 new Object[]{getDisplayName(), grouperGroupInfo, tsGroup});
1881 
1882         deleteGroup(grouperGroupInfo, tsGroup);
1883         return true;
1884       }
1885     }
1886 
1887     Set<Member> groupMembers = grouperGroupInfo.getMembers();
1888     Set<Subject> correctSubjects = new HashSet<Subject>();
1889     
1890     for (Member member : groupMembers) {
1891       Subject subject = member.getSubject();
1892       if ( subject.getTypeName().equalsIgnoreCase(SubjectTypeEnum.PERSON.getName()) ) {
1893         correctSubjects.add(subject);
1894       }
1895     }
1896 
1897     if ( correctSubjects.size() > 0 )
1898       prepareUserCache(correctSubjects, oldestCacheTimeAllowed);
1899     
1900     Set<TSUserClass> correctTSUsers = new HashSet<TSUserClass>();
1901     
1902     if ( getConfig().needsTargetSystemUsers() ) {
1903       // Loop through a copy of the correct subjects so that we can remove 
1904       // subjects that don't have matching TargetSystem users.
1905       for ( Subject correctSubject: new ArrayList<Subject>(correctSubjects) ) {
1906         TSUserClass tsUser = tsUserCache_shortTerm.get(correctSubject);
1907         if ( tsUser == null ) {
1908           // User is necessary in target system, but is not present
1909           LOG.warn("{}: Member in grouper group {} is being ignored because subject is not present in target system: {}",
1910               getDisplayName(), grouperGroupInfo, correctSubject);
1911           
1912           correctSubjects.remove(correctSubject);
1913         }
1914         else {
1915           correctTSUsers.add(tsUser);
1916         }
1917       }
1918     }
1919 
1920     LOG.debug("{}/{}: All correct member subjects: {}",
1921               new Object[] {getDisplayName(), grouperGroupInfo, correctSubjects});
1922 
1923     LOG.info("{}/{}: {} correct member subjects. Sample: {}...",
1924       new Object[] {getDisplayName(), grouperGroupInfo, correctSubjects.size(),
1925               new ArrayList<Subject>(correctSubjects).subList(0, Math.min(10, correctSubjects.size()))});
1926 
1927     try {
1928       MDC.put("step", "prov/");
1929       return doFullSync(grouperGroupInfo, tsGroup, correctSubjects, tsUserCache_shortTerm, correctTSUsers, stats);
1930     }
1931     finally {
1932       MDC.remove("step");
1933     }
1934   }
1935 
1936   /**
1937    * Get the ProvisioningWorkItem that this provisioner is currently processing
1938    * @return
1939    */
1940   public ProvisioningWorkItem getCurrentWorkItem() {
1941     return currentWorkItem.get();
1942   }
1943   
1944   public void setCurrentWorkItem(ProvisioningWorkItem item) {
1945     currentWorkItem.set(item);
1946   }
1947   
1948   protected static String getSubjectCacheKey(String subjectId, String sourceId) {
1949     String cacheKey = String.format("%s__%s", sourceId, subjectId);
1950     return cacheKey;
1951   }
1952 	  
1953   protected static String getSubjectCacheKey(Subject subject) {
1954     String cacheKey = getSubjectCacheKey(subject.getSourceId(), subject.getSourceId());
1955     return cacheKey;
1956   }
1957   
1958   protected Subject getSubject(String subjectId, String sourceId) {
1959     String cacheKey = getSubjectCacheKey(subjectId, sourceId);
1960     
1961     Subject subject = grouperSubjectCache.get(cacheKey);
1962     
1963     if ( subject != null ) 
1964       return subject;
1965     
1966       subject = SubjectFinder.findByIdAndSource(subjectId, sourceId, false);
1967 
1968       if ( subject != null )
1969         grouperSubjectCache.put(cacheKey, subject);
1970       
1971       return subject;
1972   }
1973 
1974   protected GrouperGroupInfo getGroupInfoOfExistingGroup(Group group) {
1975     String groupName = group.getName();
1976     GrouperGroupInfo result = grouperGroupInfoCache.get(groupName);
1977     if ( result == null ) {
1978       result = new GrouperGroupInfo(group);
1979       grouperGroupInfoCache.put(groupName, result);
1980     }
1981     return result;
1982   }
1983   
1984   
1985   protected GrouperGroupInfo getGroupInfoOfExistingGroup(String groupName) {
1986     GrouperGroupInfo grouperGroupInfo = grouperGroupInfoCache.get(groupName);
1987 
1988     // Return group if it was cached
1989     if (grouperGroupInfo != null)
1990       return grouperGroupInfo;
1991 
1992     try {
1993       // Look for an existing grouper group
1994       Group group = GroupFinder.findByName(GrouperSession.staticGrouperSession(false), groupName, false);
1995 
1996       if (group != null) {
1997         return getGroupInfoOfExistingGroup(group);
1998       }
1999     } catch (GroupNotFoundException e) {
2000       LOG.warn("Unable to find existing group '{}'", groupName);
2001     }
2002     return null;
2003   }
2004 
2005   protected GrouperGroupInfo getGroupInfo(ProvisioningWorkItem workItem) {
2006     String groupName = workItem.groupName;
2007 
2008     if ( groupName == null ) {
2009       return null;
2010     }
2011 
2012     GrouperGroupInfo result = getGroupInfoOfExistingGroup(groupName);
2013     if ( result != null ) {
2014       LOG.debug("{}: Found existing group {}", getDisplayName(), result);
2015       return result;
2016     }
2017     PITGroup pitGroup = null;
2018     try {
2019       // If an existing grouper group wasn't found, look for a PITGroup
2020         pitGroup = PITGroupFinder.findMostRecentByName(groupName, false);
2021     }
2022     catch (GroupNotFoundException e) {
2023       LOG.warn("Unable to find PIT group '{}'", groupName);
2024     }
2025     if ( pitGroup != null ) {
2026         result = new GrouperGroupInfo(pitGroup);
2027         result.idIndex = workItem.getGroupIdIndex();
2028 
2029         // Avoiding caching pitGroup-based items because they are workItem dependent
2030         //grouperGroupInfoCache.put(groupName, result);
2031         LOG.debug("{}: Found PIT group {}", getDisplayName(), result);
2032 
2033         return result;
2034     } else {
2035       LOG.warn("Could not find PIT group: {}", groupName);
2036       result = new GrouperGroupInfo(groupName, workItem.getGroupIdIndex());
2037       LOG.debug("{}: Using barebones group info {}", getDisplayName(), result);
2038       return result;
2039     }
2040     
2041   }
2042   
2043   private FullSyncProvisioner getFullSyncer() throws PspException {
2044     return FullSyncProvisionerFactory.getFullSyncer(this);
2045   }
2046 
2047 
2048   /**
2049    * This method looks for groups that are marked for provisioning as determined by 
2050    * the GroupSelectionExpression. 
2051    * 
2052    * Because it can take a very long time to look through a large group registry, this
2053    * method pre-filters groups and folders before evaluating them with the GroupSelectionExpression:
2054    * This method looks for Groups and Folders that reference the attributes in 
2055    * attributesUsedInGroupSelectionExpression
2056    * 
2057    * @return A collection of groups that are to be provisioned by this provisioner
2058    */
2059   public Set<GrouperGroupInfo> getAllGroupsForProvisioner2()  {
2060     Date start = new Date();
2061 
2062     LOG.debug("{}: Compiling a list of all groups selected for provisioning", getDisplayName());
2063     Set<GrouperGroupInfo> result = new HashSet<>();
2064 
2065     Set<Group> interestingGroups = new HashSet<Group>();
2066     
2067     Map<String, Set<String>> attributeToGroupNameAssigned = new HashMap<String, Set<String>>();
2068     Map<String, Set<String>> attributeToStemNameAssigned = new HashMap<String, Set<String>>();
2069     
2070     for ( String attribute : getConfig().getAttributesUsedInGroupSelectionExpression() ) {
2071       Set<Stem> foldersReferencingAttribute;
2072       Set<Group> groupsReferencingAttribute;
2073 
2074       if ( getConfig().areAttributesUsedInGroupSelectionExpressionComparedToProvisionerName() ) {
2075         LOG.debug("Looking for folders that match attribute {}={}", attribute, getConfigName());
2076         foldersReferencingAttribute = new StemFinder().assignNameOfAttributeDefName(attribute).assignAttributeValue(getConfigName()).findStems();
2077         LOG.debug("Looking for groups that match attribute {}={}", attribute, getConfigName());
2078         groupsReferencingAttribute = new GroupFinder().assignNameOfAttributeDefName(attribute).assignAttributeValue(getConfigName()).findGroups();
2079       }
2080       else {
2081         LOG.debug("Looking for folders that have attribute {}", attribute);
2082         foldersReferencingAttribute = new StemFinder().assignNameOfAttributeDefName(attribute).findStems();
2083         LOG.debug("Looking for groups that have attribute {}", attribute);
2084         groupsReferencingAttribute = new GroupFinder().assignNameOfAttributeDefName(attribute).findGroups();
2085       }
2086 
2087       {
2088         Set<String> groupNames = attributeToGroupNameAssigned.get(attribute);
2089         if (groupNames == null) {
2090           groupNames = new HashSet<String>();
2091           attributeToGroupNameAssigned.put(attribute, groupNames);
2092         }
2093         for (Group group : GrouperUtil.nonNull(groupsReferencingAttribute)) {
2094           groupNames.add(group.getName());
2095         }
2096       }
2097       {
2098         Set<String> stemNames = attributeToStemNameAssigned.get(attribute);
2099         if (stemNames == null) {
2100           stemNames = new HashSet<String>();
2101           attributeToStemNameAssigned.put(attribute, stemNames);
2102         }
2103         for (Stem stem : GrouperUtil.nonNull(foldersReferencingAttribute)) {
2104           stemNames.add(stem.getName());
2105         }
2106       }
2107       
2108       LOG.debug("{}: There are {} folders that match {} attribute", new Object[]{getDisplayName(), foldersReferencingAttribute.size(), attribute});
2109       LOG.debug("{}: There are {} groups that match {} attribute", new Object[]{getDisplayName(), groupsReferencingAttribute.size(), attribute});
2110 
2111       interestingGroups.addAll(groupsReferencingAttribute);
2112       for ( Stem folder : foldersReferencingAttribute ) {
2113         Set<Group> groupsUnderFolder;
2114 
2115         groupsUnderFolder = new GroupFinder().assignParentStemId(folder.getId()).assignStemScope(Scope.SUB).findGroups();
2116 
2117         LOG.debug("{}: There are {} groups underneath folder {}", new Object[]{getDisplayName(), groupsUnderFolder.size(), folder.getName()});
2118         interestingGroups.addAll(groupsUnderFolder);
2119       }
2120     }
2121 
2122     boolean pspngNonScriptProvisionable = GrouperLoaderConfig.retrieveConfig().propertyValueBoolean("pspngNonScriptProvisionable", true);
2123     
2124     String provisionToName = GrouperConfig.retrieveConfig().propertyValueString(
2125         "grouper.rootStemForBuiltinObjects", "etc") + ":pspng:provision_to";
2126     String doNotProvisionToName = GrouperConfig.retrieveConfig().propertyValueString(
2127         "grouper.rootStemForBuiltinObjects", "etc") + ":pspng:do_not_provision_to";
2128 
2129     for ( Group group : interestingGroups ) {
2130       GrouperGroupInforGroupInfo.html#GrouperGroupInfo">GrouperGroupInfo grouperGroupInfo = new GrouperGroupInfo(group);
2131       if (pspngNonScriptProvisionable && StringUtils.equals(config.getGroupSelectionExpression(), config.groupSelectionExpression_defaultValue())) {
2132         Boolean provisionable = null;
2133         if (grouperGroupInfo.hasGroupBeenDeleted() ) {
2134           provisionable = false;
2135         }
2136         final String objectName = group.getName();
2137         // first look for not provisionable
2138         if (provisionable == null && attributeToGroupNameAssigned.get(doNotProvisionToName).contains(objectName)) {
2139           provisionable = false;
2140         }
2141         //lets walk up the folder structure and look for an assignment
2142         if (provisionable == null) {
2143           String currentObjectNamePointer = objectName;
2144           for (int i=0;i<1000;i++) {
2145             currentObjectNamePointer = GrouperUtil.parentStemNameFromName(currentObjectNamePointer, false);
2146             if (provisionable == null && attributeToStemNameAssigned.get(doNotProvisionToName).contains(currentObjectNamePointer)) {
2147               provisionable = false;
2148               break;
2149             }
2150             if (":".equals(currentObjectNamePointer) || "".equals(currentObjectNamePointer)) {
2151               break;
2152             }
2153           }
2154         }
2155         // now look for provisionable
2156         if (provisionable == null && attributeToGroupNameAssigned.get(provisionToName).contains(objectName)) {
2157           provisionable = true;
2158         }
2159         //lets walk up the folder structure and look for an assignment
2160         if (provisionable == null) {
2161           String currentObjectNamePointer = objectName;
2162           for (int i=0;i<1000;i++) {
2163             currentObjectNamePointer = GrouperUtil.parentStemNameFromName(currentObjectNamePointer, false);
2164             if (provisionable == null && attributeToStemNameAssigned.get(provisionToName).contains(currentObjectNamePointer)) {
2165               provisionable = true;
2166               break;
2167             }
2168             if (":".equals(currentObjectNamePointer) || "".equals(currentObjectNamePointer)) {
2169               break;
2170             }
2171           }
2172         }
2173         if (provisionable != null && provisionable) {
2174           result.add(grouperGroupInfo);
2175         }
2176       } else {
2177         if ( shouldGroupBeProvisioned(grouperGroupInfo) ) {
2178           result.add(grouperGroupInfo);
2179         }
2180       }
2181     }
2182     LOG.info("{}: There are {} groups selected for provisioning (found in {})",
2183         getDisplayName(), result.size(), PspUtils.formatElapsedTime(start, null));
2184 
2185     return result;
2186   }
2187   
2188   private static ExpirableCache<String, Set<GrouperGroupInfo>> allGroupsForProvisionerCache = null;
2189 
2190   public static void allGroupsForProvisionerFromCacheClear(String provisionerName) {
2191     if (allGroupsForProvisionerCache != null) {
2192       allGroupsForProvisionerCache.put(provisionerName, null);
2193     }
2194   }
2195 
2196   public Set<GrouperGroupInfo> allGroupsForProvisionerFromCache(String provisionerName) {
2197     int pspngCacheAllGroupProvisionableMinutes = GrouperLoaderConfig.retrieveConfig().propertyValueInt("pspngCacheAllGroupProvisionableMinutes", 2);
2198     if (pspngCacheAllGroupProvisionableMinutes <= 0) {
2199       return null;
2200     }
2201     if (allGroupsForProvisionerCache == null) {
2202       allGroupsForProvisionerCache = new ExpirableCache<String, Set<GrouperGroupInfo>>(pspngCacheAllGroupProvisionableMinutes);
2203     }
2204     Set<GrouperGroupInfo> result = allGroupsForProvisionerCache.get(provisionerName);
2205     return result;
2206   }
2207   
2208   /**
2209    * This method looks for groups that are marked for provisioning as determined by 
2210    * the GroupSelectionExpression. 
2211    * 
2212    * Because it can take a very long time to look through a large group registry, this
2213    * method pre-filters groups and folders before evaluating them with the GroupSelectionExpression:
2214    * This method looks for Groups and Folders that reference the attributes in 
2215    * attributesUsedInGroupSelectionExpression
2216    * 
2217    * @return A collection of groups that are to be provisioned by this provisioner
2218    */
2219   public Set<GrouperGroupInfo> getAllGroupsForProvisioner()  {
2220     Set<GrouperGroupInfo> result = allGroupsForProvisionerFromCache(this.provisionerConfigName);
2221     
2222     if (result != null) {
2223       return result;
2224     }
2225     
2226     boolean pspngCacheGroupProvisionable = GrouperLoaderConfig.retrieveConfig().propertyValueBoolean("pspngCacheGroupProvisionable", true);
2227     if (pspngCacheGroupProvisionable) {
2228       result = getAllGroupsForProvisioner2();
2229       if (this.allGroupsForProvisionerCache != null) {
2230         this.allGroupsForProvisionerCache.put(this.provisionerConfigName, result);
2231       }
2232       return result;
2233     }
2234 
2235     Date start = new Date();
2236 
2237     LOG.debug("{}: Compiling a list of all groups selected for provisioning", getDisplayName());
2238     result = new HashSet<>();
2239 
2240     Set<Group> interestingGroups = new HashSet<Group>();
2241     for ( String attribute : getConfig().getAttributesUsedInGroupSelectionExpression() ) {
2242       Set<Stem> foldersReferencingAttribute;
2243       Set<Group> groupsReferencingAttribute;
2244 
2245       if ( getConfig().areAttributesUsedInGroupSelectionExpressionComparedToProvisionerName() ) {
2246         LOG.debug("Looking for folders that match attribute {}={}", attribute, getConfigName());
2247         foldersReferencingAttribute = new StemFinder().assignNameOfAttributeDefName(attribute).assignAttributeValue(getConfigName()).findStems();
2248         LOG.debug("Looking for groups that match attribute {}={}", attribute, getConfigName());
2249         groupsReferencingAttribute = new GroupFinder().assignNameOfAttributeDefName(attribute).assignAttributeValue(getConfigName()).findGroups();
2250       }
2251       else {
2252         LOG.debug("Looking for folders that have attribute {}", attribute);
2253         foldersReferencingAttribute = new StemFinder().assignNameOfAttributeDefName(attribute).findStems();
2254         LOG.debug("Looking for groups that have attribute {}", attribute);
2255         groupsReferencingAttribute = new GroupFinder().assignNameOfAttributeDefName(attribute).findGroups();
2256       }
2257 
2258       LOG.debug("{}: There are {} folders that match {} attribute", new Object[]{getDisplayName(), foldersReferencingAttribute.size(), attribute});
2259       LOG.debug("{}: There are {} groups that match {} attribute", new Object[]{getDisplayName(), groupsReferencingAttribute.size(), attribute});
2260 
2261       interestingGroups.addAll(groupsReferencingAttribute);
2262       for ( Stem folder : foldersReferencingAttribute ) {
2263         Set<Group> groupsUnderFolder;
2264 
2265         groupsUnderFolder = new GroupFinder().assignParentStemId(folder.getId()).assignStemScope(Scope.SUB).findGroups();
2266 
2267         LOG.debug("{}: There are {} groups underneath folder {}", new Object[]{getDisplayName(), groupsUnderFolder.size(), folder.getName()});
2268         interestingGroups.addAll(groupsUnderFolder);
2269       }
2270     }
2271 
2272     for ( Group group : interestingGroups ) {
2273       GrouperGroupInforGroupInfo.html#GrouperGroupInfo">GrouperGroupInfo grouperGroupInfo = new GrouperGroupInfo(group);
2274       if ( shouldGroupBeProvisioned(grouperGroupInfo) )
2275         result.add(grouperGroupInfo);
2276     }
2277     LOG.info("{}: There are {} groups selected for provisioning (found in %s)",
2278         getDisplayName(), result.size(), PspUtils.formatElapsedTime(start, null));
2279 
2280     if (this.allGroupsForProvisionerCache != null) {
2281       this.allGroupsForProvisionerCache.put(this.provisionerConfigName, result);
2282     }
2283     return result;
2284   }
2285   
2286   
2287   public ConfigurationClass getConfig() {
2288     return config;
2289   }
2290   
2291   /**
2292    * This returns the configuration class needed by provisioners of this class.
2293    * Unfortunately, java generics do not allow the generics to be used in static
2294    * methods.
2295    * 
2296    * Therefore, every (concrete (non-abstract)) subclass of Provisioner needs
2297    * to implement this so the ProvisionerConfiguration subclass it needs can be 
2298    * returned.
2299    * 
2300    * TODO: Maybe this could be done with an annotation?
2301    * 
2302    * @return
2303    */
2304   public static Class<? extends ProvisionerConfiguration> getPropertyClass() {
2305     return ProvisionerConfiguration.class;
2306   }
2307 
2308   protected Map<String, Object> getGroupJexlMap(String expression, GrouperGroupInfo grouperGroupInfo) {
2309     
2310     Map<String, Object> result = grouperGroupInfo.getJexlMap();
2311 
2312     boolean pspngCacheGroupAndStemAttributes = GrouperLoaderConfig.retrieveConfig().propertyValueBoolean("pspngCacheGroupAndStemAttributes", true);
2313 
2314     if (!pspngCacheGroupAndStemAttributes) {
2315       // just do old behavior
2316       Map<String, Object> stemAttributes = PspUtils.getStemAttributes(grouperGroupInfo.getGrouperGroup());
2317       result.put("stemAttributes", stemAttributes);
2318       Map<String, Object> groupAttributes = PspUtils.getGroupAttributes(grouperGroupInfo.getGrouperGroup());
2319       result.put("groupAttributes", groupAttributes);
2320     } else {
2321       
2322       // first off, if it doesnt look like the expression even uses attribute, then forget it
2323       if (expression != null && expression.contains("stemAttributes")) {
2324         
2325         Map<String, Object> stemAttributes = PspUtils.getStemAttributes(grouperGroupInfo.getGrouperGroup());
2326         result.put("stemAttributes", stemAttributes);
2327       }
2328       
2329       // first off, if it doesnt look like the expression even uses attribute, then forget it
2330       if (expression != null && expression.contains("groupAttributes")) {
2331 
2332         Map<String, Object> groupAttributes = PspUtils.getGroupAttributes(grouperGroupInfo.getGrouperGroup());
2333         result.put("groupAttributes", groupAttributes);
2334       }
2335     }
2336     return result;
2337   }
2338   
2339   /**
2340    * Evaluate the GroupSelectionExpression to see if group should be processed by this
2341    * provisioner.
2342    * 
2343    * @param grouperGroupInfo
2344    * @return
2345    */
2346   protected boolean shouldGroupBeProvisionedConsiderCache(GrouperGroupInfo grouperGroupInfo) {
2347     boolean provisionable = false;
2348     int pspngCacheAllGroupProvisionableMinutes = GrouperLoaderConfig.retrieveConfig().propertyValueInt("pspngCacheAllGroupProvisionableMinutes", 2);
2349     
2350     boolean pspngNonScriptProvisionable = GrouperLoaderConfig.retrieveConfig().propertyValueBoolean("pspngNonScriptProvisionable", true);
2351 
2352     // if getting all provisionable groups is more efficient
2353     if (pspngCacheAllGroupProvisionableMinutes > 0 && pspngNonScriptProvisionable 
2354         && StringUtils.equals(config.getGroupSelectionExpression(), config.groupSelectionExpression_defaultValue())) {
2355       provisionable = this.getAllGroupsForProvisioner().contains(grouperGroupInfo);
2356       
2357     } else {
2358       provisionable = this.shouldGroupBeProvisioned(grouperGroupInfo);
2359     }
2360     
2361     return provisionable;
2362   }
2363 
2364   /**
2365    * Evaluate the GroupSelectionExpression to see if group should be processed by this
2366    * provisioner.
2367    * 
2368    * @param grouperGroupInfo
2369    * @return
2370    */
2371   protected boolean shouldGroupBeProvisioned(GrouperGroupInfo grouperGroupInfo) {
2372     if ( grouperGroupInfo.hasGroupBeenDeleted() ) {
2373       return false;
2374     }
2375 
2376     try {
2377       String resultString = evaluateJexlExpression("GroupSelection", config.getGroupSelectionExpression(), null, null, grouperGroupInfo, null);
2378 
2379       boolean result = BooleanUtils.toBoolean(resultString);
2380 
2381       if (result)
2382         LOG.debug("{}: Group {} matches group-selection filter.", getDisplayName(), grouperGroupInfo);
2383       else
2384         LOG.trace("{}: Group {} does not match group-selection filter.", getDisplayName(), grouperGroupInfo);
2385 
2386       return result;
2387     } catch (PspException e) {
2388       LOG.warn("{}: Error evaluating groupSelection expression for group {}. Assuming group is not selected for provisioner",
2389               getDisplayName(), grouperGroupInfo, e);
2390       return false;
2391     }
2392   }
2393 
2394   public String getDisplayName() {
2395     return provisionerDisplayName;
2396   }
2397 
2398   public String getConfigName() { return provisionerConfigName; }
2399 
2400   public void provisionBatchOfItems(List<ProvisioningWorkItem> allWorkItems) {
2401     activeProvisioner.set(this);
2402     List<ProvisioningWorkItem> filteredWorkItems=null;
2403 
2404     // Mark the items as successful if we are not enabled.
2405     // Note: They are being marked as successful so that there is an easy mechanism
2406     // to get a provisioner up to date with the changelog (or other event system).
2407     // Additionally, if you just don't want to process events, then you can remove
2408     // this provisioner from the configuration.
2409     if (!config.isEnabled()) {
2410       for (ProvisioningWorkItem workItem : allWorkItems)
2411         workItem.markAsSkippedAndWarn("Provisioner %s is not enabled", getDisplayName());
2412       return;
2413     }
2414 
2415     // Let's see if there is a reason to flush our group cache. In particular,
2416     // we flush group information when groups and their attributes are edited
2417     // because we don't know what attributes of groups could be used in various
2418     // JEXL expressions
2419     MDC.put("step", "cache_eval");
2420     try {
2421       flushCachesIfNecessary(allWorkItems);
2422     } catch (PspException e) {
2423       LOG.error("Unable to evaluate our caches", e);
2424       MDC.remove("step");
2425       throw new RuntimeException("No entries provisioned. Cache evaluation failed: " + e.getMessage(), e);
2426     }
2427 
2428     // Let the provisioner filter out any unnecessary work items.
2429     // This particularly filters out groups that we're not supposed to provision
2430     MDC.put("step", "filter/");
2431     try {
2432       filteredWorkItems = filterWorkItems(allWorkItems);
2433       LOG.info("{}: {} work items need to be processed further", getDisplayName(), filteredWorkItems.size());
2434     } catch (PspException e) {
2435       LOG.error("Unable to filter the provisioning batch", e);
2436       MDC.remove("step");
2437       throw new RuntimeException("No entries provisioned. Batch-filtering failed: " + e.getMessage(), e);
2438     }
2439 
2440     // Tell the provisioner about this batch of workItems
2441     MDC.put("step", "start/");
2442     try {
2443       try {
2444         startCoordination(filteredWorkItems);
2445         startProvisioningBatch(filteredWorkItems);
2446       } catch (PspException e) {
2447         LOG.error("Unable to begin the provisioning batch", e);
2448         MDC.remove("step");
2449         throw new RuntimeException("No entries provisioned. Batch-Start failed: " + e.getMessage(), e);
2450       }
2451 
2452       // Go through the workItems that were not marked as processed by the startProvisioningBatch
2453       // and provision them
2454       for (ProvisioningWorkItem workItem : allWorkItems) {
2455         MDC.put("step", String.format("prov/%s/", workItem.getMdcLabel()));
2456         if (!workItem.hasBeenProcessed()) {
2457           try {
2458             provisionItem(workItem);
2459           } catch (PspException e) {
2460             LOG.error("Problem provisioning {}", workItem, e);
2461             workItem.markAsFailure(e.getMessage());
2462           }
2463         }
2464       }
2465 
2466       // Do 'finish' task for workItems that are not marked as processed before now
2467 
2468       MDC.put("step", "fin/");
2469       List<ProvisioningWorkItem> workItemsToFinish = new ArrayList<ProvisioningWorkItem>();
2470       try {
2471         for (ProvisioningWorkItem workItem : allWorkItems) {
2472           if (!workItem.hasBeenProcessed())
2473             workItemsToFinish.add(workItem);
2474         }
2475         finishProvisioningBatch(workItemsToFinish);
2476         finishCoordination(filteredWorkItems, true);
2477       } catch (PspException e) {
2478         LOG.error("Problem completing provisioning batch", e);
2479         for (ProvisioningWorkItem workItem : workItemsToFinish) {
2480           if (!workItem.hasBeenProcessed())
2481             workItem.markAsFailure("Unable to finish provisioning (%s)", e.getMessage());
2482         }
2483       }
2484       MDC.remove("step");
2485     }
2486     finally{
2487       finishCoordination(filteredWorkItems, false);
2488       activeProvisioner.remove();
2489     }
2490   }
2491 
2492 
2493   /**
2494    * Look at the batch of workItems and flush caches necessary to process the entries
2495    * properly. For instance, if we're being notified that an ATTRIBUTE-ASSIGNMENT has
2496    * occurred, we can flush our cache to get the best information possible.
2497    * 
2498    * @param allWorkItems
2499    * @throws PspException
2500    */
2501   protected void flushCachesIfNecessary(List<ProvisioningWorkItem> allWorkItems)  throws PspException{
2502     for (ProvisioningWorkItem workItem : allWorkItems ) {
2503 
2504       // Skip irrelevant changelog entries
2505       if (!workItem.matchesChangelogType(ChangelogHandlingConfig.allRelevantChangelogTypes)) {
2506         continue;
2507       }
2508 
2509       // Skip changelog entries that don't need cache flushing
2510       if (!workItem.matchesChangelogType(ChangelogHandlingConfig.relevantChangesThatNeedGroupCacheFlushing)) {
2511         continue;
2512       }
2513 
2514       // We know we need to flush something from the cache. If the entry is group-specific,
2515       // we'll only flush that group
2516       GrouperGroupInfo groupInfo = workItem.getGroupInfo(this);
2517       if (groupInfo != null) {
2518         uncacheGroup(groupInfo, null);
2519       } else {
2520         // Flush everything and return
2521         uncacheAllGroups();
2522         return;
2523       }
2524     }
2525   }
2526 
2527 
2528   public boolean isFullSyncMode() {
2529     return fullSyncMode;
2530   }
2531 
2532 
2533   @Override
2534   public String toString() {
2535     return String.format("%s[%s]", getClass().getSimpleName(), getDisplayName());
2536   }
2537 
2538 
2539   /**
2540    * Some changes (eg, labeling a folder for syncing) can have a large effect and are best handled with
2541    * a complete sync of all groups.
2542    * @return true if this work item should initiate a full sync of all groups
2543    */
2544   public boolean workItemMightChangeGroupSelection(ProvisioningWorkItem workItem) {
2545     // Skip if this ChangelogHandlingConfig says this doesn't affect group selection
2546     if ( !workItem.matchesChangelogType(ChangelogHandlingConfig.changelogTypesThatCanChangeGroupSelection) ) {
2547       return false;
2548     }
2549 
2550     String attributeName = workItem.getAttributeName();
2551 
2552     // If we can't figure out what the attributeName being modified is, we need to assume
2553     // GroupSelection might be changed
2554     if (StringUtils.isEmpty(attributeName)) {
2555       LOG.info("{}: Change might change group selection: {}", getDisplayName(), workItem);
2556       return true;
2557     }
2558 
2559     if ( getConfig().attributesUsedInGroupSelectionExpression.contains(attributeName) ) {
2560       LOG.info("{}: Change changes {} which might change group selection: {}",
2561               new Object[]{getDisplayName(), attributeName, workItem});
2562       return true;
2563     }
2564 
2565     return false;
2566   }
2567 
2568   /**
2569    * Were enough subjects missing from target system that we should log more information than normal
2570    * to help track down why they were missing?
2571    *
2572    * Subclasses should use this in fetchTargetSystemUsers()
2573    * @param subjectsToFetch
2574    * @param subjectsInfoFound
2575    * @return
2576    */
2577 
2578   protected boolean shouldLogAboutMissingSubjects(Collection<Subject> subjectsToFetch, Collection<?> subjectsInfoFound) {
2579     int missingResults_count = subjectsToFetch.size() - subjectsInfoFound.size();
2580     double missingResults_percentage = 100.0 * missingResults_count / subjectsToFetch.size();
2581 
2582     if (subjectsToFetch.size() > 5 && missingResults_percentage > config.getMissingSubjectsWarningThreshold_percentage()) {
2583       return true;
2584     }
2585     return false;
2586   }
2587 
2588 
2589 
2590 
2591 }