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}