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.ThreadSafety;
048import com.unboundid.util.ThreadSafetyLevel;
049
050import static com.unboundid.ldap.sdk.LDAPMessages.*;
051import static com.unboundid.util.StaticUtils.*;
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 = toLowerCase(deleteOldRDNAttr.getValue());
273        if (delOldRDNStr.equals("true"))
274        {
275          deleteOldRDN = true;
276        }
277        else if (delOldRDNStr.equals("false"))
278        {
279          deleteOldRDN = false;
280        }
281        else
282        {
283          throw new LDAPException(ResultCode.DECODING_ERROR,
284               ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get(delOldRDNStr));
285        }
286        break;
287
288      default:
289        // This should never happen.
290        throw new LDAPException(ResultCode.DECODING_ERROR,
291             ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue()));
292    }
293  }
294
295
296
297  /**
298   * Constructs a changelog entry from information contained in the provided
299   * LDIF change record.
300   *
301   * @param  changeNumber  The change number to use for the constructed
302   *                       changelog entry.
303   * @param  changeRecord  The LDIF change record with the information to
304   *                       include in the generated changelog entry.
305   *
306   * @return  The changelog entry constructed from the provided change record.
307   *
308   * @throws  LDAPException  If a problem is encountered while constructing the
309   *                         changelog entry.
310   */
311  public static ChangeLogEntry constructChangeLogEntry(final long changeNumber,
312                                    final LDIFChangeRecord changeRecord)
313         throws LDAPException
314  {
315    final Entry e =
316         new Entry(ATTR_CHANGE_NUMBER + '=' + changeNumber + ",cn=changelog");
317    e.addAttribute("objectClass", "top", "changeLogEntry");
318    e.addAttribute(new Attribute(ATTR_CHANGE_NUMBER,
319         IntegerMatchingRule.getInstance(), String.valueOf(changeNumber)));
320    e.addAttribute(new Attribute(ATTR_TARGET_DN,
321         DistinguishedNameMatchingRule.getInstance(), changeRecord.getDN()));
322    e.addAttribute(ATTR_CHANGE_TYPE, changeRecord.getChangeType().getName());
323
324    switch (changeRecord.getChangeType())
325    {
326      case ADD:
327        // The changes attribute should be an LDIF-encoded representation of the
328        // attributes from the entry, which is the LDIF representation of the
329        // entry without the first line (which contains the DN).
330        final LDIFAddChangeRecord addRecord =
331             (LDIFAddChangeRecord) changeRecord;
332        final Entry addEntry = new Entry(addRecord.getDN(),
333             addRecord.getAttributes());
334        final String[] entryLdifLines = addEntry.toLDIF(0);
335        final StringBuilder entryLDIFBuffer = new StringBuilder();
336        for (int i=1; i < entryLdifLines.length; i++)
337        {
338          entryLDIFBuffer.append(entryLdifLines[i]);
339          entryLDIFBuffer.append(EOL);
340        }
341        e.addAttribute(new Attribute(ATTR_CHANGES,
342             OctetStringMatchingRule.getInstance(),
343             entryLDIFBuffer.toString()));
344        break;
345
346      case DELETE:
347        // No additional information is needed.
348        break;
349
350      case MODIFY:
351        // The changes attribute should be an LDIF-encoded representation of the
352        // modification, with the first two lines (the DN and changetype)
353        // removed.
354        final String[] modLdifLines = changeRecord.toLDIF(0);
355        final StringBuilder modLDIFBuffer = new StringBuilder();
356        for (int i=2; i < modLdifLines.length; i++)
357        {
358          modLDIFBuffer.append(modLdifLines[i]);
359          modLDIFBuffer.append(EOL);
360        }
361        e.addAttribute(new Attribute(ATTR_CHANGES,
362             OctetStringMatchingRule.getInstance(), modLDIFBuffer.toString()));
363        break;
364
365      case MODIFY_DN:
366        final LDIFModifyDNChangeRecord modDNRecord =
367             (LDIFModifyDNChangeRecord) changeRecord;
368        e.addAttribute(new Attribute(ATTR_NEW_RDN,
369             DistinguishedNameMatchingRule.getInstance(),
370             modDNRecord.getNewRDN()));
371        e.addAttribute(new Attribute(ATTR_DELETE_OLD_RDN,
372             BooleanMatchingRule.getInstance(),
373             (modDNRecord.deleteOldRDN() ? "TRUE" : "FALSE")));
374        if (modDNRecord.getNewSuperiorDN() != null)
375        {
376          e.addAttribute(new Attribute(ATTR_NEW_SUPERIOR,
377               DistinguishedNameMatchingRule.getInstance(),
378               modDNRecord.getNewSuperiorDN()));
379        }
380        break;
381    }
382
383    return new ChangeLogEntry(e);
384  }
385
386
387
388  /**
389   * Parses the attribute list from the specified attribute in a changelog
390   * entry.
391   *
392   * @param  entry     The entry containing the data to parse.
393   * @param  attrName  The name of the attribute from which to parse the
394   *                   attribute list.
395   * @param  targetDN  The DN of the target entry.
396   *
397   * @return  The parsed attribute list.
398   *
399   * @throws  LDAPException  If an error occurs while parsing the attribute
400   *                         list.
401   */
402  protected static List<Attribute> parseAddAttributeList(final Entry entry,
403                                                         final String attrName,
404                                                         final String targetDN)
405            throws LDAPException
406  {
407    final Attribute changesAttr = entry.getAttribute(attrName);
408    if ((changesAttr == null) || (! changesAttr.hasValue()))
409    {
410      throw new LDAPException(ResultCode.DECODING_ERROR,
411                              ERR_CHANGELOG_MISSING_CHANGES.get());
412    }
413
414    final ArrayList<String> ldifLines = new ArrayList<String>();
415    ldifLines.add("dn: " + targetDN);
416
417    final StringTokenizer tokenizer =
418         new StringTokenizer(changesAttr.getValue(), "\r\n");
419    while (tokenizer.hasMoreTokens())
420    {
421      ldifLines.add(tokenizer.nextToken());
422    }
423
424    final String[] lineArray = new String[ldifLines.size()];
425    ldifLines.toArray(lineArray);
426
427    try
428    {
429      final Entry e = LDIFReader.decodeEntry(true, TrailingSpaceBehavior.RETAIN,
430           null, lineArray);
431      return Collections.unmodifiableList(
432                  new ArrayList<Attribute>(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                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<String>();
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 =
500             new ArrayList<Attribute>(mods.length);
501        for (final Modification m : mods)
502        {
503          if (! m.getModificationType().equals(ModificationType.DELETE))
504          {
505            throw new LDAPException(ResultCode.DECODING_ERROR,
506                 ERR_CHANGELOG_INVALID_DELENTRYATTRS_MOD_TYPE.get(
507                      ATTR_DELETED_ENTRY_ATTRS));
508          }
509
510          attrs.add(m.getAttribute());
511        }
512
513        return Collections.unmodifiableList(attrs);
514      }
515      catch (final LDIFException le)
516      {
517        Debug.debugException(le);
518        throw new LDAPException(ResultCode.DECODING_ERROR,
519             ERR_CHANGELOG_INVALID_DELENTRYATTRS_MODS.get(
520                  ATTR_DELETED_ENTRY_ATTRS, getExceptionMessage(le)), le);
521      }
522    }
523    else
524    {
525      final ArrayList<String> ldifLines = new ArrayList<String>();
526      ldifLines.add("dn: " + targetDN);
527
528      final StringTokenizer tokenizer =
529           new StringTokenizer(deletedEntryAttrs.getValue(), "\r\n");
530      while (tokenizer.hasMoreTokens())
531      {
532        ldifLines.add(tokenizer.nextToken());
533      }
534
535      final String[] lineArray = new String[ldifLines.size()];
536      ldifLines.toArray(lineArray);
537
538      try
539      {
540        final Entry e = LDIFReader.decodeEntry(true,
541             TrailingSpaceBehavior.RETAIN, null, lineArray);
542        return Collections.unmodifiableList(
543                    new ArrayList<Attribute>(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, getExceptionMessage(le)), le);
551      }
552    }
553  }
554
555
556
557  /**
558   * Parses the modification list from a changelog entry representing a modify
559   * operation.
560   *
561   * @param  entry     The entry containing the data to parse.
562   * @param  targetDN  The DN of the target entry.
563   *
564   * @return  The parsed modification list, or {@code null} if the changelog
565   *          entry does not include any modifications.
566   *
567   * @throws  LDAPException  If an error occurs while parsing the modification
568   *                         list.
569   */
570  private static List<Modification> parseModificationList(final Entry entry,
571                                                          final String targetDN)
572          throws LDAPException
573  {
574    final Attribute changesAttr = entry.getAttribute(ATTR_CHANGES);
575    if ((changesAttr == null) || (! changesAttr.hasValue()))
576    {
577      return null;
578    }
579
580    final byte[] valueBytes = changesAttr.getValueByteArray();
581    if (valueBytes.length == 0)
582    {
583      return null;
584    }
585
586
587    final ArrayList<String> ldifLines = new ArrayList<String>();
588    ldifLines.add("dn: " + targetDN);
589    ldifLines.add("changetype: modify");
590
591    // Even though it's a violation of the specification in
592    // draft-good-ldap-changelog, it appears that some servers (e.g., Sun DSEE)
593    // may terminate the changes value with a null character (\u0000).  If that
594    // is the case, then we'll need to strip it off before trying to parse it.
595    final StringTokenizer tokenizer;
596    if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00))
597    {
598      final String fullValue = changesAttr.getValue();
599      final String realValue = fullValue.substring(0, fullValue.length()-2);
600      tokenizer = new StringTokenizer(realValue, "\r\n");
601    }
602    else
603    {
604      tokenizer = new StringTokenizer(changesAttr.getValue(), "\r\n");
605    }
606
607    while (tokenizer.hasMoreTokens())
608    {
609      ldifLines.add(tokenizer.nextToken());
610    }
611
612    final String[] lineArray = new String[ldifLines.size()];
613    ldifLines.toArray(lineArray);
614
615    try
616    {
617      final LDIFModifyChangeRecord changeRecord =
618           (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray);
619      return Collections.unmodifiableList(
620                  Arrays.asList(changeRecord.getModifications()));
621    }
622    catch (final LDIFException le)
623    {
624      Debug.debugException(le);
625      throw new LDAPException(ResultCode.DECODING_ERROR,
626           ERR_CHANGELOG_CANNOT_PARSE_MOD_LIST.get(ATTR_CHANGES,
627                getExceptionMessage(le)),
628           le);
629    }
630  }
631
632
633
634  /**
635   * Retrieves the change number for this changelog entry.
636   *
637   * @return  The change number for this changelog entry.
638   */
639  public final long getChangeNumber()
640  {
641    return changeNumber;
642  }
643
644
645
646  /**
647   * Retrieves the target DN for this changelog entry.
648   *
649   * @return  The target DN for this changelog entry.
650   */
651  public final String getTargetDN()
652  {
653    return targetDN;
654  }
655
656
657
658  /**
659   * Retrieves the change type for this changelog entry.
660   *
661   * @return  The change type for this changelog entry.
662   */
663  public final ChangeType getChangeType()
664  {
665    return changeType;
666  }
667
668
669
670  /**
671   * Retrieves the attribute list for an add changelog entry.
672   *
673   * @return  The attribute list for an add changelog entry, or {@code null} if
674   *          this changelog entry does not represent an add operation.
675   */
676  public final List<Attribute> getAddAttributes()
677  {
678    if (changeType == ChangeType.ADD)
679    {
680      return attributes;
681    }
682    else
683    {
684      return null;
685    }
686  }
687
688
689
690  /**
691   * Retrieves the list of deleted entry attributes for a delete changelog
692   * entry.  Note that this is a non-standard extension implemented by some
693   * types of servers and is not defined in draft-good-ldap-changelog and may
694   * not be provided by some servers.
695   *
696   * @return  The delete entry attribute list for a delete changelog entry, or
697   *          {@code null} if this changelog entry does not represent a delete
698   *          operation or no deleted entry attributes were included in the
699   *          changelog entry.
700   */
701  public final List<Attribute> getDeletedEntryAttributes()
702  {
703    if (changeType == ChangeType.DELETE)
704    {
705      return attributes;
706    }
707    else
708    {
709      return null;
710    }
711  }
712
713
714
715  /**
716   * Retrieves the list of modifications for a modify changelog entry.  Note
717   * some directory servers may also include changes for modify DN change
718   * records if there were updates to operational attributes (e.g.,
719   * modifiersName and modifyTimestamp).
720   *
721   * @return  The list of modifications for a modify (or possibly modify DN)
722   *          changelog entry, or {@code null} if this changelog entry does
723   *          not represent a modify operation or a modify DN operation with
724   *          additional changes.
725   */
726  public final List<Modification> getModifications()
727  {
728    return modifications;
729  }
730
731
732
733  /**
734   * Retrieves the new RDN for a modify DN changelog entry.
735   *
736   * @return  The new RDN for a modify DN changelog entry, or {@code null} if
737   *          this changelog entry does not represent a modify DN operation.
738   */
739  public final String getNewRDN()
740  {
741    return newRDN;
742  }
743
744
745
746  /**
747   * Indicates whether the old RDN value(s) should be removed from the entry
748   * targeted by this modify DN changelog entry.
749   *
750   * @return  {@code true} if the old RDN value(s) should be removed from the
751   *          entry, or {@code false} if not or if this changelog entry does not
752   *          represent a modify DN operation.
753   */
754  public final boolean deleteOldRDN()
755  {
756    return deleteOldRDN;
757  }
758
759
760
761  /**
762   * Retrieves the new superior DN for a modify DN changelog entry.
763   *
764   * @return  The new superior DN for a modify DN changelog entry, or
765   *          {@code null} if there is no new superior DN, or if this changelog
766   *          entry does not represent a modify DN operation.
767   */
768  public final String getNewSuperior()
769  {
770    return newSuperior;
771  }
772
773
774
775  /**
776   * Retrieves the DN of the entry after the change has been processed.  For an
777   * add or modify operation, the new DN will be the same as the target DN.  For
778   * a modify DN operation, the new DN will be constructed from the original DN,
779   * the new RDN, and the new superior DN.  For a delete operation, it will be
780   * {@code null} because the entry will no longer exist.
781   *
782   * @return  The DN of the entry after the change has been processed, or
783   *          {@code null} if the entry no longer exists.
784   */
785  public final String getNewDN()
786  {
787    switch (changeType)
788    {
789      case ADD:
790      case MODIFY:
791        return targetDN;
792
793      case MODIFY_DN:
794        // This will be handled below.
795        break;
796
797      case DELETE:
798      default:
799        return null;
800    }
801
802    try
803    {
804      final RDN parsedNewRDN = new RDN(newRDN);
805
806      if (newSuperior == null)
807      {
808        final DN parsedTargetDN = new DN(targetDN);
809        final DN parentDN = parsedTargetDN.getParent();
810        if (parentDN == null)
811        {
812          return new DN(parsedNewRDN).toString();
813        }
814        else
815        {
816          return new DN(parsedNewRDN, parentDN).toString();
817        }
818      }
819      else
820      {
821        final DN parsedNewSuperior = new DN(newSuperior);
822        return new DN(parsedNewRDN, parsedNewSuperior).toString();
823      }
824    }
825    catch (final Exception e)
826    {
827      // This should never happen.
828      Debug.debugException(e);
829      return null;
830    }
831  }
832
833
834
835  /**
836   * Retrieves an LDIF change record that is analogous to the operation
837   * represented by this changelog entry.
838   *
839   * @return  An LDIF change record that is analogous to the operation
840   *          represented by this changelog entry.
841   */
842  public final LDIFChangeRecord toLDIFChangeRecord()
843  {
844    switch (changeType)
845    {
846      case ADD:
847        return new LDIFAddChangeRecord(targetDN, attributes);
848
849      case DELETE:
850        return new LDIFDeleteChangeRecord(targetDN);
851
852      case MODIFY:
853        return new LDIFModifyChangeRecord(targetDN, modifications);
854
855      case MODIFY_DN:
856        return new LDIFModifyDNChangeRecord(targetDN, newRDN, deleteOldRDN,
857                                            newSuperior);
858
859      default:
860        // This should never happen.
861        return null;
862    }
863  }
864
865
866
867  /**
868   * Processes the operation represented by this changelog entry using the
869   * provided LDAP connection.
870   *
871   * @param  connection  The connection (or connection pool) to use to process
872   *                     the operation.
873   *
874   * @return  The result of processing the operation.
875   *
876   * @throws  LDAPException  If the operation could not be processed
877   *                         successfully.
878   */
879  public final LDAPResult processChange(final LDAPInterface connection)
880         throws LDAPException
881  {
882    switch (changeType)
883    {
884      case ADD:
885        return connection.add(targetDN, attributes);
886
887      case DELETE:
888        return connection.delete(targetDN);
889
890      case MODIFY:
891        return connection.modify(targetDN, modifications);
892
893      case MODIFY_DN:
894        return connection.modifyDN(targetDN, newRDN, deleteOldRDN, newSuperior);
895
896      default:
897        // This should never happen.
898        return null;
899    }
900  }
901}