001/*
002 * Copyright 2007-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2007-2020 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2007-2020 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk;
037
038
039
040import java.nio.charset.StandardCharsets;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.Collections;
044import java.util.List;
045import java.util.StringTokenizer;
046
047import com.unboundid.ldif.LDIFAddChangeRecord;
048import com.unboundid.ldif.LDIFChangeRecord;
049import com.unboundid.ldif.LDIFDeleteChangeRecord;
050import com.unboundid.ldif.LDIFException;
051import com.unboundid.ldif.LDIFModifyChangeRecord;
052import com.unboundid.ldif.LDIFModifyDNChangeRecord;
053import com.unboundid.ldif.LDIFReader;
054import com.unboundid.ldif.TrailingSpaceBehavior;
055import com.unboundid.ldap.matchingrules.BooleanMatchingRule;
056import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
057import com.unboundid.ldap.matchingrules.IntegerMatchingRule;
058import com.unboundid.ldap.matchingrules.OctetStringMatchingRule;
059import com.unboundid.util.Debug;
060import com.unboundid.util.NotExtensible;
061import com.unboundid.util.NotMutable;
062import com.unboundid.util.NotNull;
063import com.unboundid.util.Nullable;
064import com.unboundid.util.StaticUtils;
065import com.unboundid.util.ThreadSafety;
066import com.unboundid.util.ThreadSafetyLevel;
067
068import static com.unboundid.ldap.sdk.LDAPMessages.*;
069
070
071
072/**
073 * This class provides a data structure for representing a changelog entry as
074 * described in draft-good-ldap-changelog.  Changelog entries provide
075 * information about a change (add, delete, modify, or modify DN) operation
076 * that was processed in the directory server.  Changelog entries may be
077 * parsed from entries, and they may be converted to LDIF change records or
078 * processed as LDAP operations.
079 */
080@NotExtensible()
081@NotMutable()
082@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
083public class ChangeLogEntry
084       extends ReadOnlyEntry
085{
086  /**
087   * The name of the attribute that contains the change number that identifies
088   * the change and the order it was processed in the server.
089   */
090  @NotNull public static final String ATTR_CHANGE_NUMBER = "changeNumber";
091
092
093
094  /**
095   * The name of the attribute that contains the DN of the entry targeted by
096   * the change.
097   */
098  @NotNull public static final String ATTR_TARGET_DN = "targetDN";
099
100
101
102  /**
103   * The name of the attribute that contains the type of change made to the
104   * target entry.
105   */
106  @NotNull public static final String ATTR_CHANGE_TYPE = "changeType";
107
108
109
110  /**
111   * The name of the attribute used to hold a list of changes.  For an add
112   * operation, this will be an LDIF representation of the attributes that make
113   * up the entry.  For a modify operation, this will be an LDIF representation
114   * of the changes to the target entry.
115   */
116  @NotNull public static final String ATTR_CHANGES = "changes";
117
118
119
120  /**
121   * The name of the attribute used to hold the new RDN for a modify DN
122   * operation.
123   */
124  @NotNull public static final String ATTR_NEW_RDN = "newRDN";
125
126
127
128  /**
129   * The name of the attribute used to hold the flag indicating whether the old
130   * RDN value(s) should be removed from the target entry for a modify DN
131   * operation.
132   */
133  @NotNull public static final String ATTR_DELETE_OLD_RDN = "deleteOldRDN";
134
135
136
137  /**
138   * The name of the attribute used to hold the new superior DN for a modify DN
139   * operation.
140   */
141  @NotNull public static final String ATTR_NEW_SUPERIOR = "newSuperior";
142
143
144
145  /**
146   * The name of the attribute used to hold information about attributes from a
147   * deleted entry, if available.
148   */
149  @NotNull public static final String ATTR_DELETED_ENTRY_ATTRS =
150       "deletedEntryAttrs";
151
152
153
154  /**
155   * The serial version UID for this serializable class.
156   */
157  private static final long serialVersionUID = -4018129098468341663L;
158
159
160
161  // Indicates whether to delete the old RDN value(s) in a modify DN operation.
162  private final boolean deleteOldRDN;
163
164  // The change type for this changelog entry.
165  @NotNull private final ChangeType changeType;
166
167  // A list of the attributes for an add, or the deleted entry attributes for a
168  // delete operation.
169  @Nullable private final List<Attribute> attributes;
170
171  // A list of the modifications for a modify operation.
172  @Nullable private final List<Modification> modifications;
173
174  // The change number for the changelog entry.
175  private final long changeNumber;
176
177  // The new RDN for a modify DN operation.
178  @Nullable private final String newRDN;
179
180  // The new superior DN for a modify DN operation.
181  @Nullable private final String newSuperior;
182
183  // The DN of the target entry.
184  @NotNull private final String targetDN;
185
186
187
188  /**
189   * Creates a new changelog entry from the provided entry.
190   *
191   * @param  entry  The entry from which to create this changelog entry.
192   *
193   * @throws  LDAPException  If the provided entry cannot be parsed as a
194   *                         changelog entry.
195   */
196  public ChangeLogEntry(@NotNull final Entry entry)
197         throws LDAPException
198  {
199    super(entry);
200
201
202    final Attribute changeNumberAttr = entry.getAttribute(ATTR_CHANGE_NUMBER);
203    if ((changeNumberAttr == null) || (! changeNumberAttr.hasValue()))
204    {
205      throw new LDAPException(ResultCode.DECODING_ERROR,
206                              ERR_CHANGELOG_NO_CHANGE_NUMBER.get());
207    }
208
209    try
210    {
211      changeNumber = Long.parseLong(changeNumberAttr.getValue());
212    }
213    catch (final NumberFormatException nfe)
214    {
215      Debug.debugException(nfe);
216      throw new LDAPException(ResultCode.DECODING_ERROR,
217           ERR_CHANGELOG_INVALID_CHANGE_NUMBER.get(changeNumberAttr.getValue()),
218           nfe);
219    }
220
221
222    final Attribute targetDNAttr = entry.getAttribute(ATTR_TARGET_DN);
223    if ((targetDNAttr == null) || (! targetDNAttr.hasValue()))
224    {
225      throw new LDAPException(ResultCode.DECODING_ERROR,
226                              ERR_CHANGELOG_NO_TARGET_DN.get());
227    }
228    targetDN = targetDNAttr.getValue();
229
230
231    final Attribute changeTypeAttr = entry.getAttribute(ATTR_CHANGE_TYPE);
232    if ((changeTypeAttr == null) || (! changeTypeAttr.hasValue()))
233    {
234      throw new LDAPException(ResultCode.DECODING_ERROR,
235                              ERR_CHANGELOG_NO_CHANGE_TYPE.get());
236    }
237    changeType = ChangeType.forName(changeTypeAttr.getValue());
238    if (changeType == null)
239    {
240      throw new LDAPException(ResultCode.DECODING_ERROR,
241           ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue()));
242    }
243
244
245    switch (changeType)
246    {
247      case ADD:
248        attributes    = parseAddAttributeList(entry, ATTR_CHANGES, targetDN);
249        modifications = null;
250        newRDN        = null;
251        deleteOldRDN  = false;
252        newSuperior   = null;
253        break;
254
255      case DELETE:
256        attributes    = parseDeletedAttributeList(entry, targetDN);
257        modifications = null;
258        newRDN        = null;
259        deleteOldRDN  = false;
260        newSuperior   = null;
261        break;
262
263      case MODIFY:
264        attributes    = null;
265        modifications = parseModificationList(entry, targetDN);
266        newRDN        = null;
267        deleteOldRDN  = false;
268        newSuperior   = null;
269        break;
270
271      case MODIFY_DN:
272        attributes    = null;
273        modifications = parseModificationList(entry, targetDN);
274        newSuperior   = getAttributeValue(ATTR_NEW_SUPERIOR);
275
276        final Attribute newRDNAttr = getAttribute(ATTR_NEW_RDN);
277        if ((newRDNAttr == null) || (! newRDNAttr.hasValue()))
278        {
279          throw new LDAPException(ResultCode.DECODING_ERROR,
280                                  ERR_CHANGELOG_MISSING_NEW_RDN.get());
281        }
282        newRDN = newRDNAttr.getValue();
283
284        final Attribute deleteOldRDNAttr = getAttribute(ATTR_DELETE_OLD_RDN);
285        if ((deleteOldRDNAttr == null) || (! deleteOldRDNAttr.hasValue()))
286        {
287          throw new LDAPException(ResultCode.DECODING_ERROR,
288                                  ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get());
289        }
290        final String delOldRDNStr =
291             StaticUtils.toLowerCase(deleteOldRDNAttr.getValue());
292        if (delOldRDNStr.equals("true"))
293        {
294          deleteOldRDN = true;
295        }
296        else if (delOldRDNStr.equals("false"))
297        {
298          deleteOldRDN = false;
299        }
300        else
301        {
302          throw new LDAPException(ResultCode.DECODING_ERROR,
303               ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get(delOldRDNStr));
304        }
305        break;
306
307      default:
308        // This should never happen.
309        throw new LDAPException(ResultCode.DECODING_ERROR,
310             ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue()));
311    }
312  }
313
314
315
316  /**
317   * Constructs a changelog entry from information contained in the provided
318   * LDIF change record.
319   *
320   * @param  changeNumber  The change number to use for the constructed
321   *                       changelog entry.
322   * @param  changeRecord  The LDIF change record with the information to
323   *                       include in the generated changelog entry.
324   *
325   * @return  The changelog entry constructed from the provided change record.
326   *
327   * @throws  LDAPException  If a problem is encountered while constructing the
328   *                         changelog entry.
329   */
330  @NotNull()
331  public static ChangeLogEntry constructChangeLogEntry(final long changeNumber,
332                     @NotNull final LDIFChangeRecord changeRecord)
333         throws LDAPException
334  {
335    final Entry e =
336         new Entry(ATTR_CHANGE_NUMBER + '=' + changeNumber + ",cn=changelog");
337    e.addAttribute("objectClass", "top", "changeLogEntry");
338    e.addAttribute(new Attribute(ATTR_CHANGE_NUMBER,
339         IntegerMatchingRule.getInstance(), String.valueOf(changeNumber)));
340    e.addAttribute(new Attribute(ATTR_TARGET_DN,
341         DistinguishedNameMatchingRule.getInstance(), changeRecord.getDN()));
342    e.addAttribute(ATTR_CHANGE_TYPE, changeRecord.getChangeType().getName());
343
344    switch (changeRecord.getChangeType())
345    {
346      case ADD:
347        // The changes attribute should be an LDIF-encoded representation of the
348        // attributes from the entry, which is the LDIF representation of the
349        // entry without the first line (which contains the DN).
350        final LDIFAddChangeRecord addRecord =
351             (LDIFAddChangeRecord) changeRecord;
352        final Entry addEntry = new Entry(addRecord.getDN(),
353             addRecord.getAttributes());
354        final String[] entryLdifLines = addEntry.toLDIF(0);
355        final StringBuilder entryLDIFBuffer = new StringBuilder();
356        for (int i=1; i < entryLdifLines.length; i++)
357        {
358          entryLDIFBuffer.append(entryLdifLines[i]);
359          entryLDIFBuffer.append(StaticUtils.EOL);
360        }
361        e.addAttribute(new Attribute(ATTR_CHANGES,
362             OctetStringMatchingRule.getInstance(),
363             entryLDIFBuffer.toString()));
364        break;
365
366      case DELETE:
367        // No additional information is needed.
368        break;
369
370      case MODIFY:
371        // The changes attribute should be an LDIF-encoded representation of the
372        // modification, with the first two lines (the DN and changetype)
373        // removed.
374        final String[] modLdifLines = changeRecord.toLDIF(0);
375        final StringBuilder modLDIFBuffer = new StringBuilder();
376        for (int i=2; i < modLdifLines.length; i++)
377        {
378          modLDIFBuffer.append(modLdifLines[i]);
379          modLDIFBuffer.append(StaticUtils.EOL);
380        }
381        e.addAttribute(new Attribute(ATTR_CHANGES,
382             OctetStringMatchingRule.getInstance(), modLDIFBuffer.toString()));
383        break;
384
385      case MODIFY_DN:
386        final LDIFModifyDNChangeRecord modDNRecord =
387             (LDIFModifyDNChangeRecord) changeRecord;
388        e.addAttribute(new Attribute(ATTR_NEW_RDN,
389             DistinguishedNameMatchingRule.getInstance(),
390             modDNRecord.getNewRDN()));
391        e.addAttribute(new Attribute(ATTR_DELETE_OLD_RDN,
392             BooleanMatchingRule.getInstance(),
393             (modDNRecord.deleteOldRDN() ? "TRUE" : "FALSE")));
394        if (modDNRecord.getNewSuperiorDN() != null)
395        {
396          e.addAttribute(new Attribute(ATTR_NEW_SUPERIOR,
397               DistinguishedNameMatchingRule.getInstance(),
398               modDNRecord.getNewSuperiorDN()));
399        }
400        break;
401    }
402
403    return new ChangeLogEntry(e);
404  }
405
406
407
408  /**
409   * Parses the attribute list from the specified attribute in a changelog
410   * entry.
411   *
412   * @param  entry     The entry containing the data to parse.
413   * @param  attrName  The name of the attribute from which to parse the
414   *                   attribute list.
415   * @param  targetDN  The DN of the target entry.
416   *
417   * @return  The parsed attribute list.
418   *
419   * @throws  LDAPException  If an error occurs while parsing the attribute
420   *                         list.
421   */
422  @NotNull()
423  protected static List<Attribute> parseAddAttributeList(
424                                        @NotNull final Entry entry,
425                                        @NotNull final String attrName,
426                                        @NotNull final String targetDN)
427            throws LDAPException
428  {
429    final Attribute changesAttr = entry.getAttribute(attrName);
430    if ((changesAttr == null) || (! changesAttr.hasValue()))
431    {
432      throw new LDAPException(ResultCode.DECODING_ERROR,
433                              ERR_CHANGELOG_MISSING_CHANGES.get());
434    }
435
436    final ArrayList<String> ldifLines = new ArrayList<>(20);
437    ldifLines.add("dn: " + targetDN);
438
439    final StringTokenizer tokenizer =
440         new StringTokenizer(changesAttr.getValue(), "\r\n");
441    while (tokenizer.hasMoreTokens())
442    {
443      ldifLines.add(tokenizer.nextToken());
444    }
445
446    final String[] lineArray = new String[ldifLines.size()];
447    ldifLines.toArray(lineArray);
448
449    try
450    {
451      final Entry e = LDIFReader.decodeEntry(true, TrailingSpaceBehavior.RETAIN,
452           null, lineArray);
453      return Collections.unmodifiableList(new ArrayList<>(e.getAttributes()));
454    }
455    catch (final LDIFException le)
456    {
457      Debug.debugException(le);
458      throw new LDAPException(ResultCode.DECODING_ERROR,
459           ERR_CHANGELOG_CANNOT_PARSE_ATTR_LIST.get(attrName,
460                StaticUtils.getExceptionMessage(le)),
461           le);
462    }
463  }
464
465
466
467  /**
468   * Parses the list of deleted attributes from a changelog entry representing a
469   * delete operation.  The attribute is optional, so it may not be present at
470   * all, and there are two different encodings that we need to handle.  One
471   * encoding is the same as is used for the add attribute list, and the second
472   * is similar to the encoding used for the list of changes, except that it
473   * ends with a NULL byte (0x00).
474   *
475   * @param  entry     The entry containing the data to parse.
476   * @param  targetDN  The DN of the target entry.
477   *
478   * @return  The parsed deleted attribute list, or {@code null} if the
479   *          changelog entry does not include a deleted attribute list.
480   *
481   * @throws  LDAPException  If an error occurs while parsing the deleted
482   *                         attribute list.
483   */
484  @NotNull()
485  private static List<Attribute> parseDeletedAttributeList(
486                                      @NotNull final Entry entry,
487                                      @NotNull final String targetDN)
488          throws LDAPException
489  {
490    final Attribute deletedEntryAttrs =
491         entry.getAttribute(ATTR_DELETED_ENTRY_ATTRS);
492    if ((deletedEntryAttrs == null) || (! deletedEntryAttrs.hasValue()))
493    {
494      return null;
495    }
496
497    final byte[] valueBytes = deletedEntryAttrs.getValueByteArray();
498    if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00))
499    {
500      final String valueStr = new String(valueBytes, 0, valueBytes.length-2,
501           StandardCharsets.UTF_8);
502
503      final ArrayList<String> ldifLines = new ArrayList<>(20);
504      ldifLines.add("dn: " + targetDN);
505      ldifLines.add("changetype: modify");
506
507      final StringTokenizer tokenizer = new StringTokenizer(valueStr, "\r\n");
508      while (tokenizer.hasMoreTokens())
509      {
510        ldifLines.add(tokenizer.nextToken());
511      }
512
513      final String[] lineArray = new String[ldifLines.size()];
514      ldifLines.toArray(lineArray);
515
516      try
517      {
518
519        final LDIFModifyChangeRecord changeRecord =
520             (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray);
521        final Modification[] mods = changeRecord.getModifications();
522        final ArrayList<Attribute> attrs = new ArrayList<>(mods.length);
523        for (final Modification m : mods)
524        {
525          if (! m.getModificationType().equals(ModificationType.DELETE))
526          {
527            throw new LDAPException(ResultCode.DECODING_ERROR,
528                 ERR_CHANGELOG_INVALID_DELENTRYATTRS_MOD_TYPE.get(
529                      ATTR_DELETED_ENTRY_ATTRS));
530          }
531
532          attrs.add(m.getAttribute());
533        }
534
535        return Collections.unmodifiableList(attrs);
536      }
537      catch (final LDIFException le)
538      {
539        Debug.debugException(le);
540        throw new LDAPException(ResultCode.DECODING_ERROR,
541             ERR_CHANGELOG_INVALID_DELENTRYATTRS_MODS.get(
542                  ATTR_DELETED_ENTRY_ATTRS,
543                  StaticUtils.getExceptionMessage(le)),
544             le);
545      }
546    }
547    else
548    {
549      final ArrayList<String> ldifLines = new ArrayList<>(20);
550      ldifLines.add("dn: " + targetDN);
551
552      final StringTokenizer tokenizer =
553           new StringTokenizer(deletedEntryAttrs.getValue(), "\r\n");
554      while (tokenizer.hasMoreTokens())
555      {
556        ldifLines.add(tokenizer.nextToken());
557      }
558
559      final String[] lineArray = new String[ldifLines.size()];
560      ldifLines.toArray(lineArray);
561
562      try
563      {
564        final Entry e = LDIFReader.decodeEntry(true,
565             TrailingSpaceBehavior.RETAIN, null, lineArray);
566        return Collections.unmodifiableList(new ArrayList<>(e.getAttributes()));
567      }
568      catch (final LDIFException le)
569      {
570        Debug.debugException(le);
571        throw new LDAPException(ResultCode.DECODING_ERROR,
572             ERR_CHANGELOG_CANNOT_PARSE_DELENTRYATTRS.get(
573                  ATTR_DELETED_ENTRY_ATTRS,
574                  StaticUtils.getExceptionMessage(le)),
575             le);
576      }
577    }
578  }
579
580
581
582  /**
583   * Parses the modification list from a changelog entry representing a modify
584   * operation.
585   *
586   * @param  entry     The entry containing the data to parse.
587   * @param  targetDN  The DN of the target entry.
588   *
589   * @return  The parsed modification list, or {@code null} if the changelog
590   *          entry does not include any modifications.
591   *
592   * @throws  LDAPException  If an error occurs while parsing the modification
593   *                         list.
594   */
595  @NotNull()
596  private static List<Modification> parseModificationList(
597                                         @NotNull final Entry entry,
598                                         @NotNull final String targetDN)
599          throws LDAPException
600  {
601    final Attribute changesAttr = entry.getAttribute(ATTR_CHANGES);
602    if ((changesAttr == null) || (! changesAttr.hasValue()))
603    {
604      return null;
605    }
606
607    final byte[] valueBytes = changesAttr.getValueByteArray();
608    if (valueBytes.length == 0)
609    {
610      return null;
611    }
612
613
614    final ArrayList<String> ldifLines = new ArrayList<>(20);
615    ldifLines.add("dn: " + targetDN);
616    ldifLines.add("changetype: modify");
617
618    // Even though it's a violation of the specification in
619    // draft-good-ldap-changelog, it appears that some servers (e.g., Sun DSEE)
620    // may terminate the changes value with a null character (\u0000).  If that
621    // is the case, then we'll need to strip it off before trying to parse it.
622    final StringTokenizer tokenizer;
623    if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00))
624    {
625      final String fullValue = changesAttr.getValue();
626      final String realValue = fullValue.substring(0, fullValue.length()-2);
627      tokenizer = new StringTokenizer(realValue, "\r\n");
628    }
629    else
630    {
631      tokenizer = new StringTokenizer(changesAttr.getValue(), "\r\n");
632    }
633
634    while (tokenizer.hasMoreTokens())
635    {
636      ldifLines.add(tokenizer.nextToken());
637    }
638
639    final String[] lineArray = new String[ldifLines.size()];
640    ldifLines.toArray(lineArray);
641
642    try
643    {
644      final LDIFModifyChangeRecord changeRecord =
645           (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray);
646      return Collections.unmodifiableList(
647                  Arrays.asList(changeRecord.getModifications()));
648    }
649    catch (final LDIFException le)
650    {
651      Debug.debugException(le);
652      throw new LDAPException(ResultCode.DECODING_ERROR,
653           ERR_CHANGELOG_CANNOT_PARSE_MOD_LIST.get(ATTR_CHANGES,
654                StaticUtils.getExceptionMessage(le)),
655           le);
656    }
657  }
658
659
660
661  /**
662   * Retrieves the change number for this changelog entry.
663   *
664   * @return  The change number for this changelog entry.
665   */
666  public final long getChangeNumber()
667  {
668    return changeNumber;
669  }
670
671
672
673  /**
674   * Retrieves the target DN for this changelog entry.
675   *
676   * @return  The target DN for this changelog entry.
677   */
678  @NotNull()
679  public final String getTargetDN()
680  {
681    return targetDN;
682  }
683
684
685
686  /**
687   * Retrieves the change type for this changelog entry.
688   *
689   * @return  The change type for this changelog entry.
690   */
691  @NotNull()
692  public final ChangeType getChangeType()
693  {
694    return changeType;
695  }
696
697
698
699  /**
700   * Retrieves the attribute list for an add changelog entry.
701   *
702   * @return  The attribute list for an add changelog entry, or {@code null} if
703   *          this changelog entry does not represent an add operation.
704   */
705  @Nullable()
706  public final List<Attribute> getAddAttributes()
707  {
708    if (changeType == ChangeType.ADD)
709    {
710      return attributes;
711    }
712    else
713    {
714      return null;
715    }
716  }
717
718
719
720  /**
721   * Retrieves the list of deleted entry attributes for a delete changelog
722   * entry.  Note that this is a non-standard extension implemented by some
723   * types of servers and is not defined in draft-good-ldap-changelog and may
724   * not be provided by some servers.
725   *
726   * @return  The delete entry attribute list for a delete changelog entry, or
727   *          {@code null} if this changelog entry does not represent a delete
728   *          operation or no deleted entry attributes were included in the
729   *          changelog entry.
730   */
731  @Nullable()
732  public final List<Attribute> getDeletedEntryAttributes()
733  {
734    if (changeType == ChangeType.DELETE)
735    {
736      return attributes;
737    }
738    else
739    {
740      return null;
741    }
742  }
743
744
745
746  /**
747   * Retrieves the list of modifications for a modify changelog entry.  Note
748   * some directory servers may also include changes for modify DN change
749   * records if there were updates to operational attributes (e.g.,
750   * modifiersName and modifyTimestamp).
751   *
752   * @return  The list of modifications for a modify (or possibly modify DN)
753   *          changelog entry, or {@code null} if this changelog entry does
754   *          not represent a modify operation or a modify DN operation with
755   *          additional changes.
756   */
757  @Nullable
758  public final List<Modification> getModifications()
759  {
760    return modifications;
761  }
762
763
764
765  /**
766   * Retrieves the new RDN for a modify DN changelog entry.
767   *
768   * @return  The new RDN for a modify DN changelog entry, or {@code null} if
769   *          this changelog entry does not represent a modify DN operation.
770   */
771  @Nullable()
772  public final String getNewRDN()
773  {
774    return newRDN;
775  }
776
777
778
779  /**
780   * Indicates whether the old RDN value(s) should be removed from the entry
781   * targeted by this modify DN changelog entry.
782   *
783   * @return  {@code true} if the old RDN value(s) should be removed from the
784   *          entry, or {@code false} if not or if this changelog entry does not
785   *          represent a modify DN operation.
786   */
787  public final boolean deleteOldRDN()
788  {
789    return deleteOldRDN;
790  }
791
792
793
794  /**
795   * Retrieves the new superior DN for a modify DN changelog entry.
796   *
797   * @return  The new superior DN for a modify DN changelog entry, or
798   *          {@code null} if there is no new superior DN, or if this changelog
799   *          entry does not represent a modify DN operation.
800   */
801  @Nullable()
802  public final String getNewSuperior()
803  {
804    return newSuperior;
805  }
806
807
808
809  /**
810   * Retrieves the DN of the entry after the change has been processed.  For an
811   * add or modify operation, the new DN will be the same as the target DN.  For
812   * a modify DN operation, the new DN will be constructed from the original DN,
813   * the new RDN, and the new superior DN.  For a delete operation, it will be
814   * {@code null} because the entry will no longer exist.
815   *
816   * @return  The DN of the entry after the change has been processed, or
817   *          {@code null} if the entry no longer exists.
818   */
819  @Nullable()
820  public final String getNewDN()
821  {
822    switch (changeType)
823    {
824      case ADD:
825      case MODIFY:
826        return targetDN;
827
828      case MODIFY_DN:
829        // This will be handled below.
830        break;
831
832      case DELETE:
833      default:
834        return null;
835    }
836
837    try
838    {
839      final RDN parsedNewRDN = new RDN(newRDN);
840
841      if (newSuperior == null)
842      {
843        final DN parsedTargetDN = new DN(targetDN);
844        final DN parentDN = parsedTargetDN.getParent();
845        if (parentDN == null)
846        {
847          return new DN(parsedNewRDN).toString();
848        }
849        else
850        {
851          return new DN(parsedNewRDN, parentDN).toString();
852        }
853      }
854      else
855      {
856        final DN parsedNewSuperior = new DN(newSuperior);
857        return new DN(parsedNewRDN, parsedNewSuperior).toString();
858      }
859    }
860    catch (final Exception e)
861    {
862      // This should never happen.
863      Debug.debugException(e);
864      return null;
865    }
866  }
867
868
869
870  /**
871   * Retrieves an LDIF change record that is analogous to the operation
872   * represented by this changelog entry.
873   *
874   * @return  An LDIF change record that is analogous to the operation
875   *          represented by this changelog entry.
876   */
877  @NotNull()
878  public final LDIFChangeRecord toLDIFChangeRecord()
879  {
880    switch (changeType)
881    {
882      case ADD:
883        return new LDIFAddChangeRecord(targetDN, attributes);
884
885      case DELETE:
886        return new LDIFDeleteChangeRecord(targetDN);
887
888      case MODIFY:
889        return new LDIFModifyChangeRecord(targetDN, modifications);
890
891      case MODIFY_DN:
892        return new LDIFModifyDNChangeRecord(targetDN, newRDN, deleteOldRDN,
893                                            newSuperior);
894
895      default:
896        // This should never happen.
897        return null;
898    }
899  }
900
901
902
903  /**
904   * Processes the operation represented by this changelog entry using the
905   * provided LDAP connection.
906   *
907   * @param  connection  The connection (or connection pool) to use to process
908   *                     the operation.
909   *
910   * @return  The result of processing the operation.
911   *
912   * @throws  LDAPException  If the operation could not be processed
913   *                         successfully.
914   */
915  @NotNull()
916  public final LDAPResult processChange(@NotNull final LDAPInterface connection)
917         throws LDAPException
918  {
919    switch (changeType)
920    {
921      case ADD:
922        return connection.add(targetDN, attributes);
923
924      case DELETE:
925        return connection.delete(targetDN);
926
927      case MODIFY:
928        return connection.modify(targetDN, modifications);
929
930      case MODIFY_DN:
931        return connection.modifyDN(targetDN, newRDN, deleteOldRDN, newSuperior);
932
933      default:
934        // This should never happen.
935        return null;
936    }
937  }
938}