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-2013 UnboundID Corp.
026     */
027    package com.unboundid.directory.sdk.sync.types;
028    
029    import java.util.Arrays;
030    import java.util.Date;
031    import java.util.Map;
032    import java.util.concurrent.ConcurrentHashMap;
033    import java.util.concurrent.atomic.AtomicLong;
034    
035    import com.unboundid.ldap.sdk.DN;
036    import com.unboundid.ldap.sdk.Entry;
037    import com.unboundid.ldap.sdk.ChangeType;
038    import com.unboundid.util.InternalUseOnly;
039    import com.unboundid.util.ThreadSafety;
040    import com.unboundid.util.ThreadSafetyLevel;
041    
042    import 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)
055    public 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    }