001package com.identityworksllc.iiq.common; 002 003import org.apache.commons.logging.Log; 004import org.apache.commons.logging.LogFactory; 005import sailpoint.Version; 006import sailpoint.api.SailPointContext; 007import sailpoint.object.SailPointObject; 008import sailpoint.plugin.PluginBaseHelper; 009import sailpoint.tools.GeneralException; 010import sailpoint.tools.Util; 011 012import java.lang.reflect.InvocationTargetException; 013import java.lang.reflect.Method; 014import java.sql.Connection; 015import java.sql.PreparedStatement; 016import java.sql.ResultSet; 017import java.sql.SQLException; 018import java.time.Instant; 019import java.util.Date; 020import java.util.Optional; 021 022/** 023 * In 8.4, a new class NamedTimestamp was added. We can't depend on it existing in 024 * earlier versions, but the behavior is so useful, we want to simulate it. 025 * 026 * In pre-8.4 versions, we will do so using a database table, iiqc_named_timestamps. 027 * In 8.4, we will simply use the object as-is via reflection. 028 */ 029public class NamedTimestampUtils { 030 /** 031 * The SQL query to delete a timestamp by name. 032 */ 033 public static final String DELETE_QUERY = "DELETE FROM iiqc_named_timestamps WHERE name = ?"; 034 035 /** 036 * The SQL query to fetch a timestamp by name. 037 */ 038 public static final String FETCH_VALUE_QUERY = "SELECT value FROM iiqc_named_timestamps WHERE name = ?"; 039 040 /** 041 * The version string for IIQ 8.4. 042 */ 043 public static final String IIQ_84 = "8.4"; 044 045 /** 046 * The SQL query to insert a timestamp by name. 047 */ 048 public static final String INSERT_QUERY = "INSERT INTO iiqc_named_timestamps (id, name, value) VALUES (?, ?, ?)"; 049 050 /** 051 * The method names for the NamedTimestamp class. 052 */ 053 public static final String METHOD_GET_TIMESTAMP = "getTimestamp"; 054 055 /** 056 * The method names for the NamedTimestamp class. 057 */ 058 public static final String METHOD_SET_TIMESTAMP = "setTimestamp"; 059 060 /** 061 * The name of the NamedTimestamp object. 062 */ 063 public static final String OBJECT_NAMED_TIMESTAMP = "sailpoint.object.NamedTimestamp"; 064 065 /** 066 * The SQL query to check if the iiqc_named_timestamps table exists. 067 */ 068 public static final String TABLE_EXISTS_QUERY = "SELECT 1 FROM iiqc_named_timestamps"; 069 070 /** 071 * The SQL query to update a timestamp by name. 072 */ 073 public static final String UPDATE_QUERY = "UPDATE iiqc_named_timestamps SET value = ? WHERE name = ?"; 074 075 /** 076 * The SailPointContext to use. 077 */ 078 private final SailPointContext context; 079 080 /** 081 * The logger to use. 082 */ 083 private final Log log; 084 085 /** 086 * Creates a new NamedTimestampUtils instance. 087 * @param context the SailPointContext to use 088 */ 089 public NamedTimestampUtils(SailPointContext context) { 090 this.context = context; 091 this.log = LogFactory.getLog(NamedTimestampUtils.class); 092 } 093 094 /** 095 * Fetches a timestamp by name. If the timestamp is not found, an empty Optional is returned. 096 * @param name The name of the timestamp 097 * @return An Optional containing the timestamp, or an empty Optional if the timestamp is not found 098 * @throws GeneralException if an error occurs during retrieval 099 */ 100 private Optional<Instant> fetch84Timestamp(String name) throws GeneralException { 101 if (log.isTraceEnabled()) { 102 log.trace("Fetching 8.4 timestamp for " + name); 103 } 104 try { 105 @SuppressWarnings("unchecked") 106 Class<? extends SailPointObject> timestampClass = (Class<? extends SailPointObject>) Class.forName(OBJECT_NAMED_TIMESTAMP); 107 108 SailPointObject namedTimestamp = context.getObjectByName(timestampClass, name); 109 110 if (namedTimestamp != null) { 111 Method getter = timestampClass.getMethod(METHOD_GET_TIMESTAMP); 112 Date timestampValue = (Date) getter.invoke(namedTimestamp); 113 114 if (timestampValue != null) { 115 return Optional.of(timestampValue.toInstant()); 116 } else { 117 log.warn("NamedTimestamp " + name + " did not have a timestamp value??"); 118 } 119 } else { 120 log.info("NamedTimestamp " + name + " not found"); 121 } 122 } catch(ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 123 log.error("Failure getting 8.4 named timestamp", e); 124 throw new GeneralException(e); 125 } 126 127 return Optional.empty(); 128 } 129 130 /** 131 * Fetches a timestamp by name. If the timestamp is not found, an empty Optional is returned. 132 * @param name The name of the timestamp 133 * @return An Optional containing the timestamp, or an empty Optional if the timestamp is not found 134 * @throws GeneralException if an error occurs during retrieval 135 */ 136 public Optional<Instant> get(String name) throws GeneralException { 137 if (Version.getFullVersion().contains(IIQ_84)) { 138 Optional<Instant> result = fetch84Timestamp(name); 139 if (!result.isPresent() && tableExists()) { 140 result = fetchTimestampFromDatabase(name); 141 } 142 return result; 143 } else if (tableExists()) { 144 return fetchTimestampFromDatabase(name); 145 } else { 146 throw new GeneralException("NamedTimestamp table does not exist and 8.4 timestamp class is not available"); 147 } 148 } 149 150 /** 151 * Fetches a timestamp by name from the custom database table. 152 * @param name the name of the timestamp 153 * @return an Optional containing the timestamp, or an empty Optional if the timestamp is not found 154 * @throws GeneralException if an error occurs during retrieval 155 */ 156 private Optional<Instant> fetchTimestampFromDatabase(String name) throws GeneralException { 157 if (log.isTraceEnabled()) { 158 log.trace("Fetching database timestamp for " + name); 159 } 160 161 try (Connection connection = PluginBaseHelper.getConnection()) { 162 try (PreparedStatement stmt = connection.prepareStatement(FETCH_VALUE_QUERY)) { 163 stmt.setString(1, name); 164 165 try (ResultSet results = stmt.executeQuery()) { 166 if (results.next()) { 167 return Optional.of(results.getTimestamp(1).toInstant()); 168 } else { 169 log.info("NamedTimestamp " + name + " not found"); 170 } 171 } 172 } 173 } catch(SQLException e) { 174 throw new RuntimeException(e); 175 } 176 177 return Optional.empty(); 178 } 179 180 /** 181 * Removes a timestamp from the database. 182 * @param name the name of the timestamp to remove 183 * @throws GeneralException if an error occurs during removal 184 */ 185 private void removeTimestampFromDatabase(String name) throws GeneralException { 186 if (!tableExists()) { 187 return; 188 } 189 190 if (log.isTraceEnabled()) { 191 log.trace("Removing timestamp from database: " + name); 192 } 193 194 try (Connection connection = PluginBaseHelper.getConnection()) { 195 try (PreparedStatement stmt = connection.prepareStatement(DELETE_QUERY)) { 196 stmt.setString(1, name); 197 stmt.executeUpdate(); 198 } 199 } catch(SQLException e) { 200 log.error("Error removing timestamp from database", e); 201 throw new GeneralException(e); 202 } 203 204 } 205 206 /** 207 * Attempts to store the timestamp using IIQ 8.4's NamedTimestamp object. 208 * 209 * @param name the name of the timestamp 210 * @param timestamp the timestamp to store 211 * @throws GeneralException if an error occurs during storage 212 */ 213 private void store84Timestamp(String name, Instant timestamp) throws GeneralException { 214 if (log.isTraceEnabled()) { 215 log.trace("Storing 8.4 timestamp: " + name + ", value = " + timestamp); 216 } 217 218 try { 219 @SuppressWarnings("unchecked") 220 Class<? extends SailPointObject> timestampClass = (Class<? extends SailPointObject>) Class.forName(OBJECT_NAMED_TIMESTAMP); 221 222 SailPointObject namedTimestamp = context.getObjectByName(timestampClass, name); 223 224 if (namedTimestamp == null) { 225 // Create a new NamedTimestamp object 226 namedTimestamp = timestampClass.getConstructor().newInstance(); 227 namedTimestamp.setName(name); 228 } 229 230 // Set the timestamp 231 Method setTimestampMethod = timestampClass.getMethod(METHOD_SET_TIMESTAMP, Date.class); 232 setTimestampMethod.invoke(namedTimestamp, Date.from(timestamp)); 233 234 // Save the object 235 context.saveObject(namedTimestamp); 236 context.commitTransaction(); 237 238 } catch(ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { 239 log.error("Failure storing 8.4 named timestamp", e); 240 } 241 } 242 243 /** 244 * Stores a timestamp by name. 245 * @param name the name of the timestamp 246 * @param instant the timestamp to store 247 * @throws GeneralException if an error occurs during storage 248 */ 249 public void put(String name, Instant instant) throws GeneralException { 250 if (Util.isNullOrEmpty(name)) { 251 throw new IllegalArgumentException("Name cannot be null or empty"); 252 } 253 254 if (Version.getFullVersion().contains(IIQ_84)) { 255 store84Timestamp(name, instant); 256 removeTimestampFromDatabase(name); 257 } else if (tableExists()) { 258 storeTimestampInDatabase(name, instant); 259 } else { 260 throw new GeneralException("NamedTimestamp table does not exist and 8.4 timestamp class is not available"); 261 } 262 } 263 264 /** 265 * Stores a timestamp by name. 266 * 267 * Equivalent to calling storeTimestamp(name, Instant.ofEpochMillis(value)). 268 * 269 * @param name the name of the timestamp 270 * @param value the timestamp to store 271 * @throws GeneralException if an error occurs during storage 272 */ 273 public void put(String name, long value) throws GeneralException { 274 put(name, Instant.ofEpochMilli(value)); 275 } 276 277 /** 278 * Stores a timestamp in the custom database table. 279 * 280 * @param name the name of the timestamp 281 * @param timestamp the timestamp to store 282 * @throws GeneralException if an error occurs during storage 283 */ 284 private void storeTimestampInDatabase(String name, Instant timestamp) throws GeneralException { 285 if (log.isTraceEnabled()) { 286 log.trace("Storing timestamp in database: " + name + ", value = " + timestamp); 287 } 288 try (Connection connection = PluginBaseHelper.getConnection()) { 289 // Check if the timestamp already exists 290 boolean exists = false; 291 try (PreparedStatement stmt = connection.prepareStatement(FETCH_VALUE_QUERY)) { 292 stmt.setString(1, name); 293 try (ResultSet results = stmt.executeQuery()) { 294 if (results.next()) { 295 exists = true; 296 } 297 } 298 } 299 300 if (exists) { 301 // Update existing timestamp 302 try (PreparedStatement updateStmt = connection.prepareStatement( 303 UPDATE_QUERY)) { 304 updateStmt.setTimestamp(1, java.sql.Timestamp.from(timestamp)); 305 updateStmt.setString(2, name); 306 updateStmt.executeUpdate(); 307 } 308 } else { 309 // Insert new timestamp 310 String newId = java.util.UUID.randomUUID().toString(); 311 try (PreparedStatement insertStmt = connection.prepareStatement( 312 INSERT_QUERY)) { 313 insertStmt.setString(1, newId); 314 insertStmt.setString(2, name); 315 insertStmt.setTimestamp(3, java.sql.Timestamp.from(timestamp)); 316 insertStmt.executeUpdate(); 317 } 318 } 319 } catch(SQLException e) { 320 log.error("Error storing timestamp in database", e); 321 throw new RuntimeException(e); 322 } 323 } 324 325 /** 326 * Checks if the custom database table exists. 327 * @return true if the table exists, false otherwise 328 * @throws GeneralException if an error occurs during the check 329 */ 330 private boolean tableExists() throws GeneralException { 331 try (Connection connection = PluginBaseHelper.getConnection()) { 332 try (PreparedStatement stmt = connection.prepareStatement(TABLE_EXISTS_QUERY)) { 333 stmt.executeQuery(); 334 return true; 335 } 336 } catch(SQLException e) { 337 if (log.isTraceEnabled()) { 338 log.trace("NamedTimestamp table does not exist", e); 339 } 340 return false; 341 } 342 } 343}