001package com.identityworksllc.iiq.common.iterators;
002
003import com.identityworksllc.iiq.common.Utilities;
004import sailpoint.api.SailPointContext;
005import sailpoint.object.DynamicValue;
006import sailpoint.object.ReportColumnConfig;
007import sailpoint.object.Rule;
008import sailpoint.object.Script;
009import sailpoint.tools.Util;
010
011import java.util.Map;
012
013/**
014 * A wrapper around the various ways of structuring report columns. This object produces
015 * a standard column representation that can be consumed by various ResultSet processing
016 * classes. The properties of a column that can be represented by this class are:
017 *
018 * - <code>column</code>: a required string that can be parsed by {@link ColumnToken}
019 * - <code>ifEmpty</code>: an optional fallback column token that can be used if the first results in null
020 * - <code>field</code>: a unique name for this column
021 * - <code>header</code>: an optional header name for this column
022 * - <code>renderScript</code>: an optional Beanshell script that can be used to further render a column value
023 *
024 * An instance of this class can be constructed from:
025 *
026 * - A String column token (parsed by {@link ColumnToken})
027 * - A Map containing any of the above column arguments
028 * - An IIQ {@link ReportColumnConfig} object (e.g., from a report executor)
029 * - Another instance of this class (to copy)
030 *
031 * If a String column token is provided, the value for <code>field</code> will be the
032 * first component of the token.
033 *
034 * An instance of ReportColumnConfig may contain vastly more properties than are used
035 * here. This implementation is not guaranteed to have the same semantics as Sailpoint's
036 * reporting use of that class.
037 *
038 */
039public final class ColumnConfig {
040
041    /**
042     * The error returned if the input to the constructor is wrong
043     */
044    private static final String BAD_INPUT_ERROR = "Input must be a non-null String column token, a Sailpoint ReportColumnConfig, a Map, or a ColumnConfig";
045
046    /**
047     * The column token derived type separator
048     */
049    public static final String COLUMN_TYPE_SEPARATOR = ":";
050
051    /**
052     * The column token, which must be in a format recognized by {@link ResultSetIterator#deriveTypedValue(SailPointContext, Object, String)}}
053     */
054    private final String column;
055    /**
056     * The parsed column token, if a string column was provided
057     */
058    private ColumnToken columnToken;
059
060    /**
061     * The field name, which is used as the key for {@link ResultSetIterator#nextRow()} and can be arbitrary
062     */
063    private String field;
064    /**
065     * The header for this column, allowing friendly display of the results
066     */
067    private String header;
068    /**
069     * If not null, must contain a second column token that will be used as a fallback
070     * if the original returns null
071     */
072    private String ifEmpty;
073
074    /**
075     * The parsed column token for the ifEmpty column
076     */
077    private ColumnToken ifEmptyColumnToken;
078    /**
079     * Stores a rule that can be used to render this column further
080     */
081    private Rule renderRule;
082    /**
083     * Stores a script that can be used to render this column further
084     */
085    private Script renderScript;
086    /**
087     * A wrapped {@link ReportColumnConfig} object, allowing use of this class in
088     * report executors
089     */
090    private final ReportColumnConfig reportColumnConfig;
091
092    /**
093     * Constructs a ColumnConfig from the given input. The input must be a String, which
094     * will be interpreted as a column token, a ReportColumnConfig object, which will be
095     * taken as-is, a Map, or an instance of this class, which will be copied.
096     *
097     * If you specify a Map, it must at least contain a 'column' value.
098     *
099     * @param rcc The input argument
100     */
101    public ColumnConfig(Object rcc) {
102        this(rcc, true);
103    }
104
105    /**
106     * Constructs a ColumnConfig with a fallback property, which will be used if the
107     * main property value is null.
108     *
109     * @param column         The column token to use first
110     * @param fallbackColumn The column token to try if the first result is null (optional)
111     */
112    public ColumnConfig(String column, String fallbackColumn) {
113        // NOTE: we pass false here because we need to calculate tokens after setting isEmpty
114        this(column, false);
115        this.ifEmpty = fallbackColumn;
116
117        this.recalculateColumnTokens();
118    }
119
120    /**
121     * Internal constructor that will optionally recalculate the tokens if true
122     * is provided for the boolean parameter.
123     * @param rcc The input argument
124     * @param recalculateTokens True if we ought to recalculate tokens, false if the caller intends to do it manually
125     */
126    private ColumnConfig(Object rcc, boolean recalculateTokens) {
127        if (rcc == null) {
128            throw new IllegalArgumentException(BAD_INPUT_ERROR);
129        }
130        if (rcc instanceof ColumnConfig) {
131            ColumnConfig other = (ColumnConfig) rcc;
132            this.column = other.column;
133            this.reportColumnConfig = other.reportColumnConfig;
134            this.ifEmpty = other.ifEmpty;
135            this.header = other.header;
136            this.field = other.field;
137            this.renderScript = other.renderScript;
138            this.renderRule = other.renderRule;
139        } else if (rcc instanceof Map) {
140            @SuppressWarnings("unchecked")
141            Map<String, Object> input = (Map<String, Object>) rcc;
142            this.reportColumnConfig = null;
143            this.column = Util.otoa(input.get("column"));
144            this.ifEmpty = Util.otoa(input.get("ifEmpty"));
145            this.field = Util.otoa(input.get("field"));
146            this.header = Util.otoa(input.get("header"));
147            this.renderScript = Utilities.getAsScript(input.get("renderScript"));
148        } else {
149            // Returns null if the object is not castable to this type
150            this.reportColumnConfig = Utilities.safeCast(rcc, ReportColumnConfig.class);
151            this.column = Utilities.safeCast(rcc, String.class);
152            this.ifEmpty = null;
153        }
154
155        if (this.reportColumnConfig == null && this.column == null) {
156            throw new IllegalArgumentException(BAD_INPUT_ERROR);
157        }
158
159        if (recalculateTokens) {
160            this.recalculateColumnTokens();
161        }
162    }
163
164    /**
165     * Gets the parsed column token value
166     * @return The parsed column token value
167     */
168    public ColumnToken getColumnToken() {
169        return columnToken;
170    }
171
172    /**
173     * Gets the field name of this column. This will be the key in the Map returned from
174     * {@link ResultSetIterator#nextRow()} and {@link ResultSetIterator#getFieldHeaderMap()}.
175     * It is NOT used in any way for SQL.
176     * <p>
177     * The result will be equal to:
178     * <p>
179     * 1) The value of this object's 'field' property if {@link #withFieldName} has been used
180     * <p>
181     * 2) the value of the wrapped ReportColumnConfig's 'field'
182     * <p>
183     * 3) Otherwise, it will be the base name of the main column without any type tokens.
184     * If the property name is 'col1:timestamp:yyyy-MM-dd', the output of this method
185     * would be 'col1'.
186     * <p>
187     * All other scenarios will result in an exception.
188     *
189     * @return The field name
190     */
191    public String getField() {
192        String result = null;
193        boolean finished = false;
194        if (Util.isNotNullOrEmpty(this.field)) {
195            result = this.field;
196        } else {
197            if (reportColumnConfig != null) {
198                if (Util.isNotNullOrEmpty(reportColumnConfig.getField())) {
199                    result = reportColumnConfig.getField();
200                    finished = true;
201                }
202            }
203            if (!finished) {
204                result = this.columnToken.getBaseColumnName();
205            }
206        }
207        if (result == null) {
208            throw new IllegalStateException("Column defined as property token " + getProperty() + " produces null from getField()??");
209        }
210        return result;
211    }
212
213    /**
214     * Gets the header mapped for this field. You can get a mapping from field names
215     * to headers via {@link ResultSetIterator#getFieldHeaderMap()}.
216     * <p>
217     * The header will be derived as, in order:
218     * <p>
219     * 1. The header set via {@link #withHeader(String)}.
220     * 2. The header column on the wrapped ReportColumnConfig.
221     * 3. The result of {@link #getField()}
222     *
223     * @return The header for this column (never null)
224     */
225    public String getHeader() {
226        if (Util.isNotNullOrEmpty(header)) {
227            return header;
228        } else if (reportColumnConfig != null) {
229            if (Util.isNotNullOrEmpty(reportColumnConfig.getHeader())) {
230                return reportColumnConfig.getHeader();
231            }
232        }
233        return getField();
234    }
235
236    /**
237     * Returns the 'fallback' column token string
238     *
239     * @return The column token string to use if the main column is null
240     */
241    public String getIfEmpty() {
242        if (reportColumnConfig != null) {
243            return reportColumnConfig.getIfEmpty();
244        } else if (ifEmpty != null) {
245            return this.ifEmpty;
246        }
247        return null;
248    }
249
250    /**
251     * Gets the column token for the ifEmpty fallback column
252     * @return The column token, if one exists, or else null
253     */
254    public ColumnToken getIfEmptyColumnToken() {
255        return ifEmptyColumnToken;
256    }
257
258    /**
259     * Returns the 'main' column token, e.g., "col1" or "col1:boolean". The first
260     * part of this value will be the name of the column extracted from the SQL
261     * ResultSet. The remaining parts will be used for type derivation, if needed.
262     *
263     * This will either be the value of 'column' or the property field on a
264     * ReportColumnConfig. This is also the value parsed as the primary column
265     * token.
266     *
267     * @return The column token string to read
268     */
269    public String getProperty() {
270        if (reportColumnConfig == null) {
271            return column;
272        } else {
273            return reportColumnConfig.getProperty();
274        }
275    }
276
277    public DynamicValue getRenderDef() {
278        if (renderScript != null || renderRule != null) {
279            return new DynamicValue(renderRule, renderScript, null);
280        } else if (reportColumnConfig != null) {
281            return reportColumnConfig.getRenderDef();
282        }
283        return null;
284    }
285
286    /**
287     * Recalculates the column tokens for this column config. This should be invoked
288     * either in the constructor or manually after making changes to the class.
289     */
290    private void recalculateColumnTokens() {
291        this.columnToken = new ColumnToken(this.getProperty());
292
293        if (Util.isNotNullOrEmpty(getIfEmpty())) {
294            this.ifEmptyColumnToken = new ColumnToken(getIfEmpty());
295        }
296    }
297
298    /**
299     * Constructs a copy of this ColumnConfig with the given field name.
300     *
301     * @param fieldName The field name to set in the copied ColumnConfig
302     * @return A new ColumnConfig object with the field name set to that value
303     */
304    public ColumnConfig withFieldName(String fieldName) {
305        ColumnConfig clone = new ColumnConfig(this, false);
306        clone.field = fieldName;
307        clone.recalculateColumnTokens();
308        return clone;
309    }
310
311    /**
312     * Constructs a copy of this ColumnConfig with the given header.
313     *
314     * @param header The header to set in the copied ColumnConfig
315     * @return A new ColumnConfig object with the header
316     */
317    public ColumnConfig withHeader(String header) {
318        ColumnConfig clone = new ColumnConfig(this, true);
319        clone.header = header;
320        return clone;
321    }
322
323    /**
324     * Constructs a copy of this ColumnConfig with the given render rule. The Rule object
325     * will NOT be copied, so you must ensure that it is properly detached from the context
326     * or that the rules will always run in the context that loaded the Rule object.
327     *
328     * @param rule The rule object
329     * @return A new ColumnConfig object with the renderRule set to the given Rule
330     */
331    public ColumnConfig withRenderRule(Rule rule) {
332        ColumnConfig clone = new ColumnConfig(this, false);
333        clone.renderRule = rule;
334        clone.recalculateColumnTokens();
335        return clone;
336    }
337
338    /**
339     * Constructs a copy of this ColumnConfig with the given render script. The Script
340     * object itself will also be copied to avoid caching thread-safety problems.
341     *
342     * @param script The render script
343     * @return A new ColumnConfig object with the renderScript set to the given Script
344     */
345    public ColumnConfig withRenderScript(Script script) {
346        ColumnConfig clone = new ColumnConfig(this, false);
347        clone.renderScript = script;
348        clone.recalculateColumnTokens();
349        return clone;
350    }
351}