001package com.identityworksllc.iiq.common.iterators;
002
003import com.identityworksllc.iiq.common.Functions;
004import org.apache.commons.logging.Log;
005import org.apache.commons.logging.LogFactory;
006import sailpoint.api.SailPointContext;
007import sailpoint.api.SailPointFactory;
008import sailpoint.tools.CloseableIterator;
009import sailpoint.tools.GeneralException;
010import sailpoint.tools.Util;
011
012import java.util.Iterator;
013import java.util.Objects;
014import java.util.concurrent.Callable;
015import java.util.function.Function;
016
017/**
018 * A class that applies a transformation function to each item of an Iterator
019 * before returning it from {@link Iterator#next()}. Basically the Iterator 
020 * equivalent of {@link java.util.stream.Stream#map(Function)}.
021 *
022 * If you suppress nulls, all null transformed values will be skipped.
023 *
024 * This is a read-only iterator and does not support remove().
025 *
026 * @param <In> The input class type
027 * @param <Out> The output class type
028 */
029public class TransformingIterator<In, Out> implements AutoCloseable, CloseableIterator<Out>, Iterator<Out> {
030    /**
031     * A functional interface similar to {@link java.util.function.Function}, except throwing an exception
032     * @param <In> The input class type
033     * @param <Out> The output class type
034     */
035    @FunctionalInterface
036    public interface TransformerFunction<In, Out> {
037        /**
038         * Applies the transformation to the given input object, returning an object of the output type
039         * @param input The input object to transform
040         * @return The output object
041         * @throws GeneralException if any failures occur
042         */
043        Out apply(In input) throws GeneralException;
044    }
045    /**
046     * True after the finalizer is invoked, either on close() or the last hasNext()
047     */
048    private boolean finalized;
049    /**
050     * Something to invoke after the last item is read
051     */
052    protected Runnable finalizer;
053    /**
054     * If true, nulls output from the transformation will be dropped. This requires
055     * lookahead behavior which will slightly alter the way this iterator works.
056     */
057    private boolean ignoreNulls;
058    /**
059     * The input iterator
060     */
061    private final Iterator<? extends In> input;
062    /**
063     * Tracks the last element if ignoreNulls is enabled
064     */
065    private Out lastElement;
066    /**
067     * Logger
068     */
069    private final Log log;
070    /**
071     * True after the first call to next() or hasNext()
072     */
073    private boolean started;
074    /**
075     * The output transformation function
076     */
077    protected TransformerFunction<In, Out> transformation;
078
079    /**
080     * Constructor
081     * @param input The iterator being wrapped by this transformer
082     */
083    public TransformingIterator(Iterator<? extends In> input) {
084        this.input = input;
085        this.log = LogFactory.getLog(this.getClass());
086    }
087
088    /**
089     * Constructor
090     * @param input The iterator being wrapped by this transformer
091     * @param transformation The transformation to apply to each element of the input iterator
092     */
093    public TransformingIterator(Iterator<? extends In> input, TransformerFunction<In, Out> transformation) {
094        this.input = input;
095        this.transformation = transformation;
096        this.log = LogFactory.getLog(this.getClass());
097    }
098
099    /**
100     * @see CloseableIterator#close()
101     */
102    @Override
103    public void close() {
104        /* Do nothing */
105        if (input != null) {
106            Util.flushIterator(input);
107        }
108        runFinalizer();
109    }
110
111    /**
112     * @see Iterator#hasNext()
113     */
114    @Override
115    public boolean hasNext() {
116        boolean result;
117        Objects.requireNonNull(transformation);
118        started = true;
119        if (ignoreNulls) {
120            if (input.hasNext()) {
121                In nextItem = input.next();
122                try {
123                    Out transformed = transformation.apply(nextItem);
124                    while (transformed == null && input.hasNext()) {
125                        nextItem = input.next();
126                        transformed = transformation.apply(nextItem);
127                    }
128                    if (transformed == null) {
129                        result = false;
130                    } else {
131                        lastElement = transformed;
132                        result = true;
133                    }
134                } catch (GeneralException e) {
135                    log.error("Caught an error doing the transform in TransformingIterator", e);
136                    throw new IllegalStateException(e);
137                }
138            } else {
139                result = false;
140            }
141        } else {
142            result = input.hasNext();
143        }
144
145        if (!result) {
146            runFinalizer();
147        }
148
149        return result;
150    }
151
152    /**
153     * Sets the ignore nulls flag to true
154     */
155    public TransformingIterator<In, Out> ignoreNulls() {
156        if (started) {
157            throw new IllegalStateException("ignoreNulls() changes iterator behavior and must be called before any call to next() or hasNext()");
158        }
159        this.ignoreNulls = true;
160        return this;
161    }
162
163    /**
164     * @see Iterator#next()
165     */
166    @Override
167    public Out next() {
168        Objects.requireNonNull(transformation);
169
170        started = true;
171        if (Thread.interrupted()) {
172            throw new IllegalStateException(new InterruptedException("Thread interrupted"));
173        }
174        if (ignoreNulls) {
175            return lastElement;
176        } else {
177            In value = input.next();
178            try {
179                return transformation.apply(value);
180            } catch (GeneralException e) {
181                log.error("Caught an error doing the transform in TransformingIterator", e);
182                throw new IllegalStateException(e);
183            }
184        }
185    }
186
187    /**
188     * Runs the finalizer if it has not already been run
189     */
190    private synchronized void runFinalizer() {
191        if (!finalized && this.finalizer != null) {
192            this.finalizer.run();
193            this.finalized = true;
194        }
195    }
196
197    public TransformingIterator<In, Out> setFinalizer(Runnable finalizer) {
198        this.finalizer = finalizer;
199        return this;
200    }
201}