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}