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