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 2011-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.ldap.sdk.Entry; 037import com.unboundid.ldap.sdk.ChangeType; 038import com.unboundid.util.InternalUseOnly; 039import com.unboundid.util.ThreadSafety; 040import com.unboundid.util.ThreadSafetyLevel; 041 042import com.unboundid.directory.sdk.sync.util.ScriptUtils; 043 044/** 045 * This class represents the basis for a single change record. This is 046 * effectively a hint that a change happened, and some metadata about the 047 * change. A SyncSource implementation should create instances of this class 048 * based on changes detected in the source endpoint (either from a changelog 049 * table or some other change tracking mechanism). The resync process will also 050 * use instances of this class to identify source entries, which can then be 051 * fetched using the <code>fetchEntry()</code> method on the SyncSource 052 * extension. 053 */ 054@ThreadSafety(level = ThreadSafetyLevel.COMPLETELY_THREADSAFE) 055public final class ChangeRecord 056{ 057 058 // The basic, common attributes of a generic ChangeRecord. 059 private final long changeNumber; 060 private final ChangeType changeType; 061 private final DN identifiableInfo; 062 private final DN identifiableInfoAfterChange; 063 private final String[] changedAttributes; 064 private final String modifier; 065 private final long changeTime; 066 private final Entry fullEntry; 067 private final Map<Object, Object> properties; 068 private CompletionStatus completionStatus; 069 070 private static final AtomicLong uniqueIdSequence = new AtomicLong(0); 071 072 /** 073 * Private constructor, uses Builder to construct immutable instance. 074 * @param bldr the Builder from which to create this change record. 075 */ 076 private ChangeRecord(final Builder bldr) 077 { 078 // if changeNumber is not set, assign it an auto-incrementing value. we need 079 // this to have a way to identify a specific change and the order in which 080 // changes are processed 081 changeNumber = (bldr.changeNumber == -1) ? 082 uniqueIdSequence.getAndIncrement() : bldr.changeNumber; 083 changeType = bldr.changeType; 084 identifiableInfo = bldr.identifiableInfo; 085 identifiableInfoAfterChange = bldr.changeType == ChangeType.MODIFY_DN 086 ? bldr.identifiableInfoAfterChange : identifiableInfo; 087 changedAttributes = bldr.changedAttributes; 088 modifier = bldr.modifier; 089 changeTime = bldr.changeTime; 090 fullEntry = bldr.fullEntry; 091 properties = bldr.properties; 092 } 093 094 /** 095 * Get the change number that identifies this particular change. If a change 096 * number is not used by the source for change detection, this method will 097 * return a monotonically increasing sequence number for this change record, 098 * so that you can still identify the order in which changes were detected. 099 * @return the changeNumber 100 */ 101 public long getChangeNumber() 102 { 103 return changeNumber; 104 } 105 106 /** 107 * Get the change type (ADD/MODIFY/MOD-DN/DELETE). For <i>resync</i> 108 * operations, this will be <code>null</code>. 109 * @return the changeType 110 */ 111 public ChangeType getChangeType() 112 { 113 return changeType; 114 } 115 116 /** 117 * Get a DN that identifies the entry or record that changed (for example 118 * "accountID=123"). If multiple attributes are part of the identifier, 119 * they will be represented as different 120 * RDN components of the DN (for example "accountID=123,groupID=5"). 121 * @return an identifier DN for the entry that changed 122 */ 123 public DN getIdentifiableInfo() 124 { 125 return identifiableInfo; 126 } 127 128 /** 129 * Get a DN that identifies the entry or record after the change is 130 * complete. This DN will only differ from the result of 131 * {@code getIdentifiableInfo()} if the change type was a Modify DN. 132 * @return the DN for the entry after the change. 133 */ 134 public DN getIdentifiableInfoAfterChange() { 135 return identifiableInfoAfterChange; 136 } 137 138 /** 139 * Get the set of changed attributes for this change. 140 * @return an array of attribute names that were modified as part of the 141 * change 142 */ 143 public String[] getChangedAttributes() 144 { 145 return changedAttributes; 146 } 147 148 /** 149 * Get the user account name that made the change. 150 * @return the source user account 151 */ 152 public String getModifier() 153 { 154 return modifier; 155 } 156 157 /** 158 * Get the time at which the change occurred. 159 * @return the change time (in milliseconds since 160 * January 1, 1970 00:00:00.000 GMT) 161 */ 162 public long getChangeTime() 163 { 164 return changeTime; 165 } 166 167 /** 168 * Get the full source entry (if it was set on this ChangeRecord when it was 169 * created). Typically this will be <code>null</code>, but some extensions 170 * may opt to set the entry when the ChangeRecord is constructed in order to 171 * skip the <code>fetchEntry()</code> phase of processing. 172 * 173 * @return the full source entry, or <code>null</code> if it has not been set 174 */ 175 public Entry getFullEntry() 176 { 177 return fullEntry; 178 } 179 180 /** 181 * Get the property value (if one exists) for the given key. 182 * @param key the key for a given property to return 183 * @return the property value, or <code>null</code> if the key is null 184 */ 185 public Object getProperty(final Object key) 186 { 187 if(key == null) 188 { 189 return null; 190 } 191 return properties.get(key); 192 } 193 194 /** 195 * This method is used by the Sync Pipe to indicate if the completion status 196 * of a synchronization operation. This is for internal use only 197 * and should not be called by extension code. 198 * @param status the completion status for this ChangeRecord 199 */ 200 @InternalUseOnly 201 public void setCompletionStatus(final CompletionStatus status) 202 { 203 this.completionStatus = status; 204 } 205 206 /** 207 * Gets the completion status for this change. This will be null if the change 208 * has not finished processing yet. 209 * @return the CompletionStatus indicating whether this change completed 210 * successfully or else a reason why it failed 211 */ 212 public CompletionStatus getCompletionStatus() 213 { 214 return completionStatus; 215 } 216 217 /** 218 * This class is used to construct ChangeRecord instances. At least a 219 * {@link ChangeType} and an identifiableInfo {@link DN} are required; the 220 * rest of the parameters are optional. 221 * Arbitrary properties can also be added to the object by calling 222 * {@link #addProperty(Object, Object)}. The setter methods return the Builder 223 * instance itself, so that these calls can be chained. When finished setting 224 * up parameters, call the {@link #build()} method to create a new 225 * {@link ChangeRecord}. 226 */ 227 public static class Builder 228 { 229 // required parameters 230 private final ChangeType changeType; 231 private final DN identifiableInfo; 232 233 // optional parameters 234 private long changeNumber; 235 private String[] changedAttributes; 236 private String modifier; 237 private long changeTime; 238 private Entry fullEntry; 239 private DN identifiableInfoAfterChange; 240 241 // various other values that are attached to the change record 242 private final Map<Object, Object> properties = 243 new ConcurrentHashMap<Object, Object>(); 244 245 // flag to indicate whether this builder has been built 246 private volatile boolean built; 247 248 /** 249 * Creates a Builder which can be used to construct a ChangeRecord. 250 * @param type 251 * the ChangeType (ADD/MODIFY/MOD-DN/DELETE). This can be 252 * <code>null</code> to indicate a resync operation. 253 * @param identifiableInfo 254 * a unique identifier for the entry that changed 255 * (i.e. "accountID=123"). If multiple attributes are part of 256 * the identifier, they should be separate RDN components of the DN 257 * (i.e. "accountID=123,groupID=5"). 258 */ 259 public Builder(final ChangeType type, final DN identifiableInfo) 260 { 261 if(identifiableInfo == null) 262 { 263 throw new IllegalArgumentException( 264 "The 'identifiableInfo' parameter cannot be null."); 265 } 266 this.changeType = type; 267 this.identifiableInfo = identifiableInfo; 268 this.changeNumber = -1; 269 this.built = false; 270 } 271 272 /** 273 * Creates a Builder which can be used to construct a ChangeRecord. 274 * @param type 275 * the ChangeType (ADD/MODIFY/MOD-DN/DELETE). This can be 276 * <code>null</code> to indicate a resync operation. 277 * @param identifiableInfo 278 * a unique identifier for the entry that changed 279 * (i.e. "accountID=123"). If multiple attributes are part of 280 * the identifier, they should be delimited with the default 281 * delimiter of "%%" (i.e. "accountID=123%%groupID=5"). 282 */ 283 public Builder(final ChangeType type, final String identifiableInfo) 284 { 285 this(type, ScriptUtils.idStringToDN(identifiableInfo, null)); 286 } 287 288 /** 289 * Creates a Builder which can be used to construct a ChangeRecord. 290 * @param type 291 * the ChangeType (ADD/MODIFY/MOD-DN/DELETE). This can be 292 * <code>null</code> to indicate a resync operation. 293 * @param identifiableInfo 294 * a unique identifier for the row that changed 295 * (i.e. "accountID=123"). If multiple attributes are part of 296 * the identifier, they should be delimited with a unique string 297 * (i.e. "accountID=123%%groupID=5") which is specified by the 298 * <i>delimiter</i> parameter. 299 * @param delimiter 300 * The delimiter used to split separate components of the 301 * identifiable info. If this is null, the default of "%%" will be 302 * used. 303 */ 304 public Builder(final ChangeType type, final String identifiableInfo, 305 final String delimiter) 306 { 307 this(type, ScriptUtils.idStringToDN(identifiableInfo, delimiter)); 308 } 309 310 /** 311 * Set the change number that identifies this particular change (if 312 * applicable). If this is not used, a change number will be automatically 313 * generated for the ChangeRecord. 314 * @param changeNumber 315 * the change number 316 * @return the Builder instance 317 */ 318 public Builder changeNumber(final long changeNumber) 319 { 320 this.changeNumber = changeNumber; 321 return this; 322 } 323 324 /** 325 * Set the DN of the entry after the change. This should only be used if 326 * the change type was a Modify DN. 327 * @param identifiableInfoAfterChange The final DN of the entry after 328 * the change. 329 * @return the Builder instance. 330 * @throws IllegalArgumentException if the change type is not Modify DN. 331 */ 332 public Builder identifiableInfoAfterChange( 333 final DN identifiableInfoAfterChange) 334 { 335 if (changeType != ChangeType.MODIFY_DN) { 336 throw new IllegalArgumentException("identifiableInfoAfterChange can " + 337 "only be set for Modify DN changes."); 338 } 339 this.identifiableInfoAfterChange = identifiableInfoAfterChange; 340 return this; 341 } 342 343 /** 344 * Set the set of changed attributes for this change entry. 345 * @param changedAttributes 346 * an array of attribute names that were modified as part of the 347 * change 348 * @return the Builder instance 349 */ 350 public Builder changedAttributes(final String[] changedAttributes) 351 { 352 this.changedAttributes = changedAttributes; 353 return this; 354 } 355 356 /** 357 * Set the user account name that made the change. 358 * @param modifier 359 * the account name or user name of the entity that made the change 360 * @return the Builder instance 361 */ 362 public Builder modifier(final String modifier) 363 { 364 this.modifier = modifier; 365 return this; 366 } 367 368 /** 369 * Set the time at which the change occurred. This should be based on the 370 * clock at the source endpoint if possible. 371 * @param changeTime the time of the change (in milliseconds since 372 January 1, 1970 00:00:00.000 GMT) 373 * @return the Builder instance 374 */ 375 public Builder changeTime(final long changeTime) 376 { 377 this.changeTime = changeTime; 378 return this; 379 } 380 381 /** 382 * Set the full source entry on this ChangeRecord. This may be desirable if 383 * the source does not provide logical separation between the 384 * "change record" and the entry itself. If this is set on the ChangeRecord, 385 * the Data Sync Server will skip the call to 386 * <code>fetchEntry()</code> in your extension and instead use this 387 * {@link Entry}. 388 * <p> 389 * When using this mechanism, make sure to set it to a non-null Entry even 390 * on a DELETE, because this will be used to correlate to the destination 391 * entry to delete. 392 * @param entry the full source entry that was changed 393 * @return the Builder instance 394 */ 395 public Builder fullEntry(final Entry entry) 396 { 397 this.fullEntry = entry; 398 return this; 399 } 400 401 /** 402 * Add an arbitrary attachment or property to the ChangeRecord being 403 * built. Nor the key or the value are allowed to be <code>null</code>. 404 * @param key 405 * the key for the property 406 * @param value 407 * the value of the property 408 * @return the Builder instance 409 */ 410 public Builder addProperty(final Object key, final Object value) 411 { 412 this.properties.put(key, value); 413 return this; 414 } 415 416 /** 417 * Construct the ChangeRecord. This method may only be called once. 418 * @return a ChangeRecord instance 419 */ 420 public synchronized ChangeRecord build() 421 { 422 if(built) 423 { 424 throw new IllegalStateException("This Builder has already been built."); 425 } 426 built = true; 427 return new ChangeRecord(this); 428 } 429 } 430 431 /** 432 * {@inheritDoc} 433 */ 434 @Override 435 public String toString() 436 { 437 String changeTypeStr = changeType != null ? changeType.toString() :"resync"; 438 String fullEntryStr = fullEntry != null ? 439 ", fullEntry=" + fullEntry.toLDIFString() : ""; 440 return "ChangeRecord [changeNumber=" + changeNumber + 441 ", changeType=" + changeTypeStr + 442 ", identifiableInfo=" + identifiableInfo + 443 ", identifiableInfoAfterChange=" + identifiableInfoAfterChange + 444 ", changedAttributes=" + Arrays.toString(changedAttributes) + 445 ", modifier=" + modifier + ", changeTime=" + 446 (new Date(changeTime)).toString() + 447 ", properties=" + properties + fullEntryStr + "]"; 448 } 449}