001package com.identityworksllc.iiq.common.task; 002 003import com.identityworksllc.iiq.common.HybridObjectMatcher; 004import com.identityworksllc.iiq.common.TaskUtil; 005import com.identityworksllc.iiq.common.iterators.FilteringIterator; 006import com.identityworksllc.iiq.common.logging.SLogger; 007import sailpoint.api.IncrementalObjectIterator; 008import sailpoint.api.ObjectUtil; 009import sailpoint.api.SailPointContext; 010import sailpoint.api.Terminator; 011import sailpoint.object.*; 012import sailpoint.task.AbstractTaskExecutor; 013import sailpoint.task.TaskMonitor; 014import sailpoint.tools.GeneralException; 015import sailpoint.tools.Util; 016 017import java.util.Iterator; 018import java.util.concurrent.atomic.AtomicBoolean; 019import java.util.concurrent.atomic.AtomicInteger; 020 021/** 022 * PurgeObjectsTask is a SailPoint IIQ task executor that deletes objects of a specified type from the database. 023 * 024 * ## Features 025 * - Deletes all objects of a given type, or only those matching a filter. 026 * - Supports in-memory filtering using HybridObjectMatcher. 027 * - Can run in simulation mode to log objects that would be deleted without actually deleting them. 028 * - Tracks progress and supports early termination. 029 * 030 * ## Attributes 031 * - `objectType` (String): The SailPoint object type to purge (required). 032 * - `deleteAll` (Boolean): If true, deletes all objects of the type. If false, uses a filter (default: false). 033 * - `filter` (String): A SailPoint filter string to select objects to delete (required if deleteAll is false). 034 * - `inMemoryFilter` (Boolean): If true, applies the filter in memory (default: false). 035 * - `simulate` (Boolean): If true, only logs objects that would be deleted (default: true). 036 * 037 */ 038public class PurgeObjectsTask extends AbstractTaskExecutor { 039 /** 040 * Atomic flag to indicate if the task has been terminated early. 041 */ 042 private final AtomicBoolean terminated = new AtomicBoolean(false); 043 044 /** 045 * Logger for task events and progress. 046 */ 047 private final SLogger logger = new SLogger(PurgeObjectsTask.class); 048 049 /** 050 * Executes the purge task, deleting or simulating deletion of objects. 051 * 052 * @param context SailPointContext for database operations 053 * @param taskSchedule The schedule for this task 054 * @param taskResult The result object to record progress and output 055 * @param attributes Task attributes (see class-level docs) 056 * @throws Exception if required attributes are missing or errors occur 057 */ 058 @Override 059 public void execute(SailPointContext context, TaskSchedule taskSchedule, TaskResult taskResult, Attributes<String, Object> attributes) throws Exception { 060 TaskMonitor monitor = new TaskMonitor(context, taskResult); 061 setMonitor(monitor); 062 063 String objectType = attributes.getString("objectType"); 064 if (Util.isNullOrEmpty(objectType)) { 065 throw new GeneralException("PurgeObjectsTask requires an 'objectType' attribute"); 066 } 067 068 Class<? extends SailPointObject> objectClass = ObjectUtil.getSailPointClass(objectType); 069 070 boolean deleteAll = attributes.getBoolean("deleteAll", false); 071 String filter = attributes.getString("filter"); 072 if (Util.isNullOrEmpty(filter) && !deleteAll) { 073 throw new GeneralException("PurgeObjectsTask requires either a non-empty 'filter' attribute or 'deleteAll' set to true"); 074 } 075 076 boolean inMemoryFilter = attributes.getBoolean("inMemoryFilter", false); 077 078 boolean simulate = attributes.getBoolean("simulate", true); 079 080 if (logger.isInfoEnabled()) { 081 logger.info("Starting purge of objects of type '" + objectType + "'" 082 + (deleteAll ? " (deleting all objects)" : " with filter: " + filter) 083 + (inMemoryFilter ? " using in-memory filtering" : "") 084 + (simulate ? " [DRY RUN MODE]" : "")); 085 } 086 087 Filter compiledFilter = null; 088 if (Util.isNotNullOrEmpty(filter)) { 089 compiledFilter = Filter.compile(filter); 090 } 091 092 Iterator<? extends SailPointObject> objectsIterator; 093 094 if (deleteAll) { 095 QueryOptions qo = new QueryOptions(); 096 objectsIterator = new IncrementalObjectIterator<>(context, objectClass, qo); 097 } else { 098 if (inMemoryFilter) { 099 QueryOptions qo = new QueryOptions(); 100 HybridObjectMatcher matcher = new HybridObjectMatcher(context, compiledFilter); 101 Iterator<? extends SailPointObject> tempIterator = new IncrementalObjectIterator<>(context, objectClass, qo); 102 objectsIterator = new FilteringIterator<>(tempIterator, (spo) -> { 103 try { 104 return matcher.matches(spo); 105 } catch (GeneralException e) { 106 logger.error("Error applying in-memory filter to object of type " + objectType + ": " + e.getMessage(), e); 107 return false; 108 } 109 }); 110 } else { 111 QueryOptions qo = new QueryOptions(); 112 qo.addFilter(compiledFilter); 113 objectsIterator = new IncrementalObjectIterator<>(context, objectClass, qo); 114 } 115 } 116 117 Terminator terminator = new Terminator(context); 118 119 AtomicInteger deleteCount = new AtomicInteger(); 120 while (objectsIterator.hasNext()) { 121 if (terminated.get()) { 122 logger.warn("PurgeObjectsTask terminated early after deleting " + deleteCount.get() + " objects."); 123 return; 124 } 125 monitor.updateProgress("Deleting object " + (deleteCount.get() + 1)); 126 SailPointObject spo = objectsIterator.next(); 127 if (!simulate) { 128 if (logger.isDebugEnabled()) { 129 logger.debug("Deleting object : " + spo.getId() + " " + spo.getName()); 130 } 131 terminator.deleteObject(objectsIterator.next()); 132 } else { 133 logger.warn("DRY RUN: Would delete object: " + spo.getId() + " " + spo.getName()); 134 } 135 deleteCount.incrementAndGet(); 136 } 137 138 TaskUtil.withLockedMasterResult(monitor, tr -> { 139 taskResult.setAttribute("deleted", deleteCount); 140 }); 141 } 142 143 /** 144 * Terminates the purge task early. 145 * 146 * @return true if termination was successful 147 */ 148 @Override 149 public boolean terminate() { 150 terminated.set(true); 151 return true; 152 } 153}