001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at 010 * docs/licenses/cddl.txt 011 * or http://www.opensource.org/licenses/cddl1.php. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * docs/licenses/cddl.txt. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2010-2021 Ping Identity Corporation 026 */ 027package com.unboundid.directory.sdk.sync.types; 028 029import java.util.Arrays; 030import java.util.Date; 031import java.util.Map; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.atomic.AtomicLong; 034 035import com.unboundid.ldap.sdk.DN; 036import com.unboundid.util.InternalUseOnly; 037import com.unboundid.util.ThreadSafety; 038import com.unboundid.util.ThreadSafetyLevel; 039 040import com.unboundid.directory.sdk.sync.util.ScriptUtils; 041 042/** 043 * This class represents the basis for a single database change record. A 044 * JDBCSyncSource implementation should create instances of this 045 * class based on changes detected in the database (either from a changelog 046 * table or some other change tracking mechanism). The resync process will also 047 * use instances of this class to identify database entries, which can then be 048 * fetched using the <code>fetchEntry()</code> method on the JDBCSyncSource 049 * extension. 050 */ 051@ThreadSafety(level = ThreadSafetyLevel.COMPLETELY_THREADSAFE) 052public final class DatabaseChangeRecord 053{ 054 055 /** Potential types of database changes. */ 056 public enum ChangeType 057 { 058 /** 059 * Represents a database insert. 060 */ 061 insert, 062 /** 063 * Represents a database delete. 064 */ 065 delete, 066 /** 067 * Represents a database update. 068 */ 069 update, 070 /** 071 * Represents a resync operation. This is a special type used by the resync 072 * command-line tool. See 073 * <code>JDBCSyncSource#listAllEntries()</code> for more 074 * information. 075 */ 076 resync 077 } 078 079 // the basic, common attributes of a DatabaseChangeRecord. 080 private final long changeNumber; 081 private final ChangeType changeType; 082 private final String tableName; 083 private final DN identifiableInfo; 084 private final String entryType; 085 private final String[] changedColumns; 086 private final String modifier; 087 private final long changeTime; 088 private final Map<Object, Object> properties; 089 private CompletionStatus completionStatus; 090 091 private static final AtomicLong uniqueIdSequence = new AtomicLong(0); 092 093 /** 094 * Private constructor, uses Builder to construct immutable instance. 095 * @param bldr the Builder from which to create this change record. 096 */ 097 private DatabaseChangeRecord(final Builder bldr) 098 { 099 // if changeNumber is not set, assign it an auto-incrementing value. we need 100 // this to have a way to identify a specific change and the order in which 101 // changes are processed 102 changeNumber = (bldr.changeNumber == -1) ? 103 uniqueIdSequence.getAndIncrement() : bldr.changeNumber; 104 changeType = bldr.changeType; 105 identifiableInfo = bldr.identifiableInfo; 106 tableName = bldr.tableName; 107 entryType = bldr.entryType; 108 changedColumns = bldr.changedColumns; 109 modifier = bldr.modifier; 110 changeTime = bldr.changeTime; 111 properties = bldr.properties; 112 } 113 114 /** 115 * Get the change number that identifies this particular change. If a change 116 * number is not used by the database for change detection, this method will 117 * return a monotonically increasing sequence number for this change record, 118 * so that you can still identify the order in which changes were detected. 119 * @return the changeNumber 120 */ 121 public long getChangeNumber() 122 { 123 return changeNumber; 124 } 125 126 /** 127 * Get the change type (insert/update/delete/resync). 128 * @return the changeType 129 */ 130 public ChangeType getChangeType() 131 { 132 return changeType; 133 } 134 135 /** 136 * Get the database table on which the change occurred. 137 * @return the table name 138 */ 139 public String getTableName() 140 { 141 return tableName; 142 } 143 144 /** 145 * Get the DN that identifies the row that changed (for example 146 * "accountID=123"). If multiple attributes are part of the identifier, 147 * they will be represented as different 148 * RDN components of the DN in the order they were originally specified 149 * (for example "accountID=123,groupID=5"). 150 * @return an identifier string 151 */ 152 public DN getIdentifiableInfo() 153 { 154 return identifiableInfo; 155 } 156 157 /** 158 * Get the database entry type that this change corresponds to (for example 159 * "account" or "subscriber"). 160 * @return the type of database entry 161 */ 162 public String getEntryType() 163 { 164 return entryType; 165 } 166 167 /** 168 * Get the set of changed columns for this change entry. 169 * @return an array of column names that were modified as part of the change 170 */ 171 public String[] getChangedColumns() 172 { 173 return changedColumns; 174 } 175 176 /** 177 * Get the database user that made the change. 178 * @return the database user account 179 */ 180 public String getModifier() 181 { 182 return modifier; 183 } 184 185 /** 186 * Get the time at which the change occurred. 187 * @return the change time (in milliseconds since 188 * January 1, 1970 00:00:00.000 GMT) 189 */ 190 public long getChangeTime() 191 { 192 return changeTime; 193 } 194 195 /** 196 * Get the property value (if one exists) for the given key. 197 * @param key the property key whose value to return 198 * @return the property value, or <code>null</code> if the key is null 199 */ 200 public Object getProperty(final Object key) 201 { 202 if(key == null) 203 { 204 return null; 205 } 206 return properties.get(key); 207 } 208 209 /** 210 * This method is used by the Sync Pipe to indicate if the completion status 211 * of a synchronization operation. This is for internal use only 212 * and should not be called by clients. 213 * @param status the completion status for this DatabaseChangeRecord 214 */ 215 @InternalUseOnly 216 public void setCompletionStatus(final CompletionStatus status) 217 { 218 this.completionStatus = status; 219 } 220 221 /** 222 * Gets the completion status for this change. This will be null if the change 223 * has not finished processing yet. 224 * @return the CompletionStatus indicating whether this change completed 225 * successfully or else a reason why it failed 226 */ 227 public CompletionStatus getCompletionStatus() 228 { 229 return completionStatus; 230 } 231 232 /** 233 * This class is used to construct DatabaseChangeRecord instances. At least a 234 * {@link ChangeType} and an identifiableInfo {@link DN} are required; the 235 * rest of the parameters are optional. 236 * Arbitrary properties can also be added to the object by calling 237 * {@link #addProperty(Object, Object)}. The setter methods return the Builder 238 * instance itself, so that these calls can be chained. When finished setting 239 * up parameters, call the {@link #build()} method to create a new 240 * {@link DatabaseChangeRecord}. 241 */ 242 public static class Builder 243 { 244 // required parameters 245 private final ChangeType changeType; 246 private final DN identifiableInfo; 247 248 // optional parameters 249 private long changeNumber; 250 private String tableName; 251 private String entryType; 252 private String[] changedColumns; 253 private String modifier; 254 private long changeTime; 255 256 // various other values that are attached to the change record 257 private final Map<Object, Object> properties = 258 new ConcurrentHashMap<Object, Object>(); 259 260 // flag to indicate whether this builder has been built 261 private volatile boolean built; 262 263 /** 264 * Creates a Builder which can be used to construct a DatabaseChangeRecord. 265 * @param type 266 * the ChangeType (insert/update/delete/resync) 267 * @param identifiableInfo 268 * a unique identifier for the row that changed 269 * (i.e. "accountID=123"). If multiple attributes are part of 270 * the identifier, they should be separate RDN components of the DN 271 * (i.e. "accountID=123,groupID=5"). 272 */ 273 public Builder(final ChangeType type, final DN identifiableInfo) 274 { 275 if(type == null || identifiableInfo == null) 276 { 277 throw new IllegalArgumentException( 278 "The 'type' and 'identifiableInfo' parameters can not be null."); 279 } 280 this.changeType = type; 281 this.identifiableInfo = identifiableInfo; 282 this.changeNumber = -1; 283 this.built = false; 284 } 285 286 /** 287 * Creates a Builder which can be used to construct a DatabaseChangeRecord. 288 * @param type 289 * the ChangeType (insert/update/delete/resync) 290 * @param identifiableInfo 291 * a unique identifier for the row that changed 292 * (i.e. "accountID=123"). If multiple attributes are part of 293 * the identifier, they should be delimited with the default 294 * delimiter of "%%" (i.e. "accountID=123%%groupID=5"). 295 */ 296 public Builder(final ChangeType type, final String identifiableInfo) 297 { 298 this(type, ScriptUtils.idStringToDN(identifiableInfo, null)); 299 } 300 301 /** 302 * Creates a Builder which can be used to construct a DatabaseChangeRecord. 303 * @param type 304 * the ChangeType (insert/update/delete/resync) 305 * @param identifiableInfo 306 * a unique identifier for the row that changed 307 * (i.e. "accountID=123"). If multiple attributes are part of 308 * the identifier, they should be delimited with a unique string 309 * (i.e. "accountID=123%%groupID=5") which is specified by the 310 * <i>delimiter</i> parameter. 311 * @param delimiter 312 * The delimiter used to split separate components of the 313 * identifiable info. If this is null, the default of "%%" will be 314 * used. 315 */ 316 public Builder(final ChangeType type, final String identifiableInfo, 317 final String delimiter) 318 { 319 this(type, ScriptUtils.idStringToDN(identifiableInfo, delimiter)); 320 } 321 322 /** 323 * Set the change number that identifies this particular change (if 324 * applicable). 325 * @param changeNumber the change number 326 * @return the Builder instance 327 */ 328 public Builder changeNumber(final long changeNumber) 329 { 330 this.changeNumber = changeNumber; 331 return this; 332 } 333 334 /** 335 * Set the database table on which the change occurred. 336 * @param tableName the table name 337 * @return the Builder instance 338 */ 339 public Builder tableName(final String tableName) 340 { 341 this.tableName = tableName; 342 return this; 343 } 344 345 /** 346 * Set the database entry type that this change corresponds to (for example 347 * "account" 348 * or "subscriber"). 349 * @param entryType the type of database entry 350 * @return the Builder instance 351 */ 352 public Builder entryType(final String entryType) 353 { 354 this.entryType = entryType; 355 return this; 356 } 357 358 /** 359 * Set the set of changed columns for this change entry. 360 * @param changedColumns 361 * an array of column names that were modified as part of the 362 * change 363 * @return the Builder instance 364 */ 365 public Builder changedColumns(final String[] changedColumns) 366 { 367 this.changedColumns = changedColumns; 368 return this; 369 } 370 371 /** 372 * Set the database user that made the change. 373 * @param modifier the database account name 374 * @return the Builder instance 375 */ 376 public Builder modifier(final String modifier) 377 { 378 this.modifier = modifier; 379 return this; 380 } 381 382 /** 383 * Set the time at which the change occurred. This should be based on the 384 * clock on the database server. 385 * @param changeTime the time of the change (in milliseconds since 386 * January 1, 1970 00:00:00.000 GMT) 387 * @return the Builder instance 388 */ 389 public Builder changeTime(final long changeTime) 390 { 391 this.changeTime = changeTime; 392 return this; 393 } 394 395 /** 396 * Add an arbitrary attachment or property to the DatabaseChangeRecord being 397 * built. Nor the key or the value are allowed to be <code>null</code>. 398 * @param key 399 * the key for the property 400 * @param value 401 * the value of the property 402 * @return the Builder instance 403 */ 404 public Builder addProperty(final Object key, final Object value) 405 { 406 this.properties.put(key, value); 407 return this; 408 } 409 410 /** 411 * Construct the DatabaseChangeRecord. This method may only be called once. 412 * @return a DatabaseChangeRecord instance 413 */ 414 public synchronized DatabaseChangeRecord build() 415 { 416 if(built) 417 { 418 throw new IllegalStateException("This Builder has already been built."); 419 } 420 built = true; 421 return new DatabaseChangeRecord(this); 422 } 423 } 424 425 /** 426 * {@inheritDoc} 427 */ 428 @Override 429 public String toString() 430 { 431 return "DatabaseChangeRecord [changeNumber=" + changeNumber + 432 ", changeType=" + 433 changeType + ", tableName=" + tableName + ", identifiableInfo=" + 434 identifiableInfo + 435 ", entryType=" + entryType + 436 ", changedColumns=" + Arrays.toString(changedColumns) + 437 ", modifier=" + modifier + ", changeTime=" + 438 (new Date(changeTime)).toString() + 439 ", properties=" + properties + "]"; 440 } 441}