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-2018 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 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}