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