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