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