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}