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}