001/*
002 * Copyright 2018-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2018-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) 2018-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.unboundidds.logs;
037
038
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collections;
043import java.util.HashSet;
044import java.util.List;
045import java.util.Set;
046
047import com.unboundid.asn1.ASN1OctetString;
048import com.unboundid.ldap.sdk.Attribute;
049import com.unboundid.ldap.sdk.ChangeType;
050import com.unboundid.ldap.sdk.DN;
051import com.unboundid.ldap.sdk.Modification;
052import com.unboundid.ldap.sdk.ModificationType;
053import com.unboundid.ldap.sdk.RDN;
054import com.unboundid.ldif.LDIFChangeRecord;
055import com.unboundid.ldif.LDIFModifyChangeRecord;
056import com.unboundid.ldif.LDIFModifyDNChangeRecord;
057import com.unboundid.ldif.LDIFException;
058import com.unboundid.ldif.LDIFReader;
059import com.unboundid.util.Debug;
060import com.unboundid.util.NotNull;
061import com.unboundid.util.Nullable;
062import com.unboundid.util.ObjectPair;
063import com.unboundid.util.StaticUtils;
064import com.unboundid.util.ThreadSafety;
065import com.unboundid.util.ThreadSafetyLevel;
066
067import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*;
068
069
070
071/**
072 * This class provides a data structure that holds information about an audit
073 * log message that represents a modify DN operation.
074 * <BR>
075 * <BLOCKQUOTE>
076 *   <B>NOTE:</B>  This class, and other classes within the
077 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
078 *   supported for use against Ping Identity, UnboundID, and
079 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
080 *   for proprietary functionality or for external specifications that are not
081 *   considered stable or mature enough to be guaranteed to work in an
082 *   interoperable way with other types of LDAP servers.
083 * </BLOCKQUOTE>
084 */
085@ThreadSafety(level= ThreadSafetyLevel.COMPLETELY_THREADSAFE)
086public final class ModifyDNAuditLogMessage
087       extends AuditLogMessage
088{
089  /**
090   * Retrieves the serial version UID for this serializable class.
091   */
092  private static final long serialVersionUID = 3954476664207635518L;
093
094
095
096  // An LDIF change record that encapsulates the change represented by this
097  // modify DN audit log message.
098  @NotNull private final LDIFModifyDNChangeRecord modifyDNChangeRecord;
099
100  // The attribute modifications associated with this modify DN operation.
101  @Nullable private final List<Modification> attributeModifications;
102
103
104
105  /**
106   * Creates a new modify DN audit log message from the provided set of lines.
107   *
108   * @param  logMessageLines  The lines that comprise the log message.  It must
109   *                          not be {@code null} or empty, and it must not
110   *                          contain any blank lines, although it may contain
111   *                          comments.  In fact, it must contain at least one
112   *                          comment line that appears before any non-comment
113   *                          lines (but possibly after other comment lines)
114   *                          that serves as the message header.
115   *
116   * @throws  AuditLogException  If a problem is encountered while processing
117   *                             the provided list of log message lines.
118   */
119  public ModifyDNAuditLogMessage(@NotNull final String... logMessageLines)
120         throws AuditLogException
121  {
122    this(StaticUtils.toList(logMessageLines), logMessageLines);
123  }
124
125
126
127  /**
128   * Creates a new modify DN audit log message from the provided set of lines.
129   *
130   * @param  logMessageLines  The lines that comprise the log message.  It must
131   *                          not be {@code null} or empty, and it must not
132   *                          contain any blank lines, although it may contain
133   *                          comments.  In fact, it must contain at least one
134   *                          comment line that appears before any non-comment
135   *                          lines (but possibly after other comment lines)
136   *                          that serves as the message header.
137   *
138   * @throws  AuditLogException  If a problem is encountered while processing
139   *                             audit provided list of log message lines.
140   */
141  public ModifyDNAuditLogMessage(@NotNull final List<String> logMessageLines)
142         throws AuditLogException
143  {
144    this(logMessageLines, StaticUtils.toArray(logMessageLines, String.class));
145  }
146
147
148
149  /**
150   * Creates a new modify DN audit log message from the provided information.
151   *
152   * @param  logMessageLineList   The lines that comprise the log message as a
153   *                              list.
154   * @param  logMessageLineArray  The lines that comprise the log message as an
155   *                              array.
156   *
157   * @throws  AuditLogException  If a problem is encountered while processing
158   *                             the provided list of log message lines.
159   */
160  private ModifyDNAuditLogMessage(
161               @NotNull final List<String> logMessageLineList,
162               @NotNull final String[] logMessageLineArray)
163          throws AuditLogException
164  {
165    super(logMessageLineList);
166
167    try
168    {
169      final LDIFChangeRecord changeRecord =
170           LDIFReader.decodeChangeRecord(logMessageLineArray);
171      if (! (changeRecord instanceof LDIFModifyDNChangeRecord))
172      {
173        throw new AuditLogException(logMessageLineList,
174             ERR_MODIFY_DN_AUDIT_LOG_MESSAGE_CHANGE_TYPE_NOT_MODIFY_DN.get(
175                  changeRecord.getChangeType().getName(),
176                  ChangeType.MODIFY_DN.getName()));
177      }
178
179      modifyDNChangeRecord = (LDIFModifyDNChangeRecord) changeRecord;
180    }
181    catch (final LDIFException e)
182    {
183      Debug.debugException(e);
184      throw new AuditLogException(logMessageLineList,
185           ERR_MODIFY_DN_AUDIT_LOG_MESSAGE_LINES_NOT_CHANGE_RECORD.get(
186                StaticUtils.getExceptionMessage(e)),
187           e);
188    }
189
190    attributeModifications =
191         decodeAttributeModifications(logMessageLineList, modifyDNChangeRecord);
192  }
193
194
195
196  /**
197   * Creates a new modify DN audit log message from the provided set of lines.
198   *
199   * @param  logMessageLines       The lines that comprise the log message.  It
200   *                               must not be {@code null} or empty, and it
201   *                               must not contain any blank lines, although it
202   *                               may contain comments.  In fact, it must
203   *                               contain at least one comment line that
204   *                               appears before any non-comment lines (but
205   *                               possibly after other comment lines) that
206   *                               serves as the message header.
207   * @param  modifyDNChangeRecord  The LDIF modify DN change record that is
208   *                               described by the provided log message lines.
209   *
210   * @throws  AuditLogException  If a problem is encountered while processing
211   *                             the provided list of log message lines.
212   */
213  ModifyDNAuditLogMessage(@NotNull final List<String> logMessageLines,
214       @NotNull final LDIFModifyDNChangeRecord modifyDNChangeRecord)
215       throws AuditLogException
216  {
217    super(logMessageLines);
218
219    this.modifyDNChangeRecord = modifyDNChangeRecord;
220
221    attributeModifications =
222         decodeAttributeModifications(logMessageLines, modifyDNChangeRecord);
223  }
224
225
226
227  /**
228   * Decodes the list of attribute modifications from the audit log message, if
229   * available.
230   *
231   * @param  logMessageLines       The lines that comprise the log message.  It
232   *                               must not be {@code null} or empty, and it
233   *                               must not contain any blank lines, although it
234   *                               may contain comments.  In fact, it must
235   *                               contain at least one comment line that
236   *                               appears before any non-comment lines (but
237   *                               possibly after other comment lines) that
238   *                               serves as the message header.
239   * @param  modifyDNChangeRecord  The LDIF modify DN change record that is
240   *                               described by the provided log message lines.
241   *
242   * @return  The list of attribute modifications from the audit log message, or
243   *          {@code null} if there were no modifications.
244   */
245  @Nullable()
246  private static List<Modification> decodeAttributeModifications(
247               @NotNull final List<String> logMessageLines,
248               @NotNull final LDIFModifyDNChangeRecord modifyDNChangeRecord)
249  {
250    List<String> ldifLines = null;
251    for (final String line : logMessageLines)
252    {
253      final String uncommentedLine;
254      if (line.startsWith("# "))
255      {
256        uncommentedLine = line.substring(2);
257      }
258      else
259      {
260        break;
261      }
262
263      if (ldifLines == null)
264      {
265        final String lowerLine = StaticUtils.toLowerCase(uncommentedLine);
266        if (lowerLine.startsWith("modifydn attribute modifications"))
267        {
268          ldifLines = new ArrayList<>(logMessageLines.size());
269        }
270      }
271      else
272      {
273        if (ldifLines.isEmpty())
274        {
275          ldifLines.add("dn: " + modifyDNChangeRecord.getDN());
276          ldifLines.add("changetype: modify");
277        }
278
279        ldifLines.add(uncommentedLine);
280      }
281    }
282
283    if (ldifLines == null)
284    {
285      return null;
286    }
287    else if (ldifLines.isEmpty())
288    {
289      return Collections.emptyList();
290    }
291    else
292    {
293      try
294      {
295        final String[] ldifLineArray =
296             ldifLines.toArray(StaticUtils.NO_STRINGS);
297        final LDIFModifyChangeRecord changeRecord =
298             (LDIFModifyChangeRecord)
299             LDIFReader.decodeChangeRecord(ldifLineArray);
300        return Collections.unmodifiableList(
301             Arrays.asList(changeRecord.getModifications()));
302      }
303      catch (final Exception e)
304      {
305        Debug.debugException(e);
306        return null;
307      }
308    }
309  }
310
311
312
313  /**
314   * {@inheritDoc}
315   */
316  @Override()
317  @NotNull()
318  public String getDN()
319  {
320    return modifyDNChangeRecord.getDN();
321  }
322
323
324
325  /**
326   * Retrieves the new RDN for the associated modify DN operation.
327   *
328   * @return  The new RDN for the associated modify DN operation.
329   */
330  @NotNull()
331  public String getNewRDN()
332  {
333    return modifyDNChangeRecord.getNewRDN();
334  }
335
336
337
338  /**
339   * Indicates whether the old RDN attribute values were removed from the entry.
340   *
341   * @return  {@code true} if the old RDN attribute values were removed from the
342   *          entry, or {@code false} if not.
343   */
344  public boolean deleteOldRDN()
345  {
346    return modifyDNChangeRecord.deleteOldRDN();
347  }
348
349
350
351  /**
352   * Retrieves the new superior DN for the associated modify DN operation, if
353   * available.
354   *
355   * @return  The new superior DN for the associated modify DN operation, or
356   *          {@code null} if there was no new superior DN.
357   */
358  @Nullable()
359  public String getNewSuperiorDN()
360  {
361    return modifyDNChangeRecord.getNewSuperiorDN();
362  }
363
364
365
366  /**
367   * Retrieves the list of attribute modifications for the associated modify DN
368   * operation, if available.
369   *
370   * @return  The list of attribute modifications for the associated modify DN
371   *          operation, or {@code null} if it is not available.  If it is
372   *          known that there were no attribute modifications, then an empty
373   *          list will be returned.
374   */
375  @Nullable()
376  public List<Modification> getAttributeModifications()
377  {
378    return attributeModifications;
379  }
380
381
382
383  /**
384   * {@inheritDoc}
385   */
386  @Override()
387  @NotNull()
388  public ChangeType getChangeType()
389  {
390    return ChangeType.MODIFY_DN;
391  }
392
393
394
395  /**
396   * {@inheritDoc}
397   */
398  @Override()
399  @NotNull()
400  public LDIFModifyDNChangeRecord getChangeRecord()
401  {
402    return modifyDNChangeRecord;
403  }
404
405
406
407  /**
408   * {@inheritDoc}
409   */
410  @Override()
411  public boolean isRevertible()
412  {
413    // We can't revert a change record if the original DN was that of the root
414    // DSE.
415    final DN parsedDN;
416    final RDN oldRDN;
417    try
418    {
419      parsedDN = modifyDNChangeRecord.getParsedDN();
420      oldRDN = parsedDN.getRDN();
421      if (oldRDN == null)
422      {
423        return false;
424      }
425    }
426    catch (final Exception e)
427    {
428      Debug.debugException(e);
429      return false;
430    }
431
432
433    // We can't create a revert change record if we can't construct the new DN
434    // for the entry.
435    final DN newDN;
436    final RDN newRDN;
437    try
438    {
439      newDN = modifyDNChangeRecord.getNewDN();
440      newRDN = modifyDNChangeRecord.getParsedNewRDN();
441    }
442    catch (final Exception e)
443    {
444      Debug.debugException(e);
445      return false;
446    }
447
448
449    // Modify DN change records will only be revertible if we have a set of
450    // attribute modifications.  If we don't have a set of attribute
451    // modifications, we can't know what value to use for the deleteOldRDN flag.
452    if (attributeModifications == null)
453    {
454      return false;
455    }
456
457
458    // If the set of attribute modifications is empty, then deleteOldRDN must
459    // be false or the new RDN must equal the old RDN.
460    if (attributeModifications.isEmpty())
461    {
462      if (modifyDNChangeRecord.deleteOldRDN() && (! newRDN.equals(oldRDN)))
463      {
464        return false;
465      }
466    }
467
468
469    // If any of the included modifications has a modification type that is
470    // anything other than add, delete, or increment, then it's not revertible.
471    // And if any of the delete modifications don't have values, then it's not
472    // revertible.
473    for (final Modification m : attributeModifications)
474    {
475      if (!ModifyAuditLogMessage.modificationIsRevertible(m))
476      {
477        return false;
478      }
479    }
480
481
482    // If we've gotten here, then we can change
483    return true;
484  }
485
486
487
488  /**
489   * {@inheritDoc}
490   */
491  @Override()
492  @NotNull()
493  public List<LDIFChangeRecord> getRevertChangeRecords()
494         throws AuditLogException
495  {
496    // We can't create a set of revertible changes if we don't have access to
497    // attribute modifications.
498    if (attributeModifications == null)
499    {
500      throw new AuditLogException(getLogMessageLines(),
501           ERR_MODIFY_DN_NOT_REVERTIBLE.get(modifyDNChangeRecord.getDN()));
502    }
503
504
505    // Get the DN of the entry after the modify DN operation was processed,
506    // along with parsed versions of the original DN, new RDN, and new superior
507    // DN.
508    final DN newDN;
509    final DN newSuperiorDN;
510    final DN originalDN;
511    final RDN newRDN;
512    try
513    {
514      newDN = modifyDNChangeRecord.getNewDN();
515      originalDN = modifyDNChangeRecord.getParsedDN();
516      newSuperiorDN = modifyDNChangeRecord.getParsedNewSuperiorDN();
517      newRDN = modifyDNChangeRecord.getParsedNewRDN();
518    }
519    catch (final Exception e)
520    {
521      Debug.debugException(e);
522
523      if (modifyDNChangeRecord.getNewSuperiorDN() == null)
524      {
525        throw new AuditLogException(getLogMessageLines(),
526             ERR_MODIFY_DN_CANNOT_GET_NEW_DN_WITHOUT_NEW_SUPERIOR.get(
527                  modifyDNChangeRecord.getDN(),
528                  modifyDNChangeRecord.getNewRDN()),
529             e);
530      }
531      else
532      {
533        throw new AuditLogException(getLogMessageLines(),
534             ERR_MODIFY_DN_CANNOT_GET_NEW_DN_WITH_NEW_SUPERIOR.get(
535                  modifyDNChangeRecord.getDN(),
536                  modifyDNChangeRecord.getNewRDN(),
537                  modifyDNChangeRecord.getNewSuperiorDN()),
538             e);
539      }
540    }
541
542
543    // If the original DN is the null DN, then fail.
544    if (originalDN.isNullDN())
545    {
546      throw new AuditLogException(getLogMessageLines(),
547           ERR_MODIFY_DN_CANNOT_REVERT_NULL_DN.get());
548    }
549
550
551    // If the set of attribute modifications is empty, then deleteOldRDN must
552    // be false or the new RDN must equal the old RDN.
553    if (attributeModifications.isEmpty())
554    {
555      if (modifyDNChangeRecord.deleteOldRDN() &&
556           (! newRDN.equals(originalDN.getRDN())))
557      {
558        throw new AuditLogException(getLogMessageLines(),
559             ERR_MODIFY_DN_CANNOT_REVERT_WITHOUT_NECESSARY_MODS.get(
560                  modifyDNChangeRecord.getDN()));
561      }
562    }
563
564
565    // Construct the DN, new RDN, and new superior DN values for the change
566    // needed to revert the modify DN operation.
567    final String revertedDN = newDN.toString();
568    final String revertedNewRDN = originalDN.getRDNString();
569
570    final String revertedNewSuperiorDN;
571    if (newSuperiorDN == null)
572    {
573      revertedNewSuperiorDN = null;
574    }
575    else
576    {
577      revertedNewSuperiorDN = originalDN.getParentString();
578    }
579
580
581    // If the set of attribute modifications is empty, then deleteOldRDN must
582    // have been false and the new RDN attribute value(s) must have already been
583    // in the entry.
584    if (attributeModifications.isEmpty())
585    {
586      return Collections.<LDIFChangeRecord>singletonList(
587           new LDIFModifyDNChangeRecord(revertedDN, revertedNewRDN, false,
588                revertedNewSuperiorDN));
589    }
590
591
592    // Iterate through the modifications to see which new RDN attributes were
593    // added to the entry.  If they were all added, then we need to use a
594    // deleteOldRDN value of true.  If none of them were added, then we need to
595    // use a deleteOldRDN value of false.  If some of them were added but some
596    // were not, then we need to use a deleteOldRDN value o false and have a
597    // second modification to delete those values that were added.
598    //
599    // Also, collect any additional modifications that don't involve new RDN
600    // attribute values.
601    final int numNewRDNs = newRDN.getAttributeNames().length;
602    final Set<ObjectPair<String,byte[]>> addedNewRDNValues =
603         new HashSet<>(StaticUtils.computeMapCapacity(numNewRDNs));
604    final RDN originalRDN = originalDN.getRDN();
605    final List<Modification> additionalModifications =
606         new ArrayList<>(attributeModifications.size());
607    final int numModifications = attributeModifications.size();
608    for (int i=numModifications - 1; i >= 0; i--)
609    {
610      final Modification m = attributeModifications.get(i);
611      if (m.getModificationType() == ModificationType.ADD)
612      {
613        final Attribute a = m.getAttribute();
614        final ArrayList<byte[]> retainedValues = new ArrayList<>(a.size());
615        for (final ASN1OctetString value : a.getRawValues())
616        {
617          final byte[] valueBytes = value.getValue();
618          if (newRDN.hasAttributeValue(a.getName(), valueBytes))
619          {
620            addedNewRDNValues.add(new ObjectPair<>(a.getName(), valueBytes));
621          }
622          else
623          {
624            retainedValues.add(valueBytes);
625          }
626        }
627
628        if (retainedValues.size() == a.size())
629        {
630          additionalModifications.add(new Modification(
631               ModificationType.DELETE, a.getName(), a.getRawValues()));
632        }
633        else if (! retainedValues.isEmpty())
634        {
635          additionalModifications.add(new Modification(
636               ModificationType.DELETE, a.getName(),
637               StaticUtils.toArray(retainedValues, byte[].class)));
638        }
639      }
640      else if (m.getModificationType() == ModificationType.DELETE)
641      {
642        final Attribute a = m.getAttribute();
643        final ArrayList<byte[]> retainedValues = new ArrayList<>(a.size());
644        for (final ASN1OctetString value : a.getRawValues())
645        {
646          final byte[] valueBytes = value.getValue();
647          if (! originalRDN.hasAttributeValue(a.getName(), valueBytes))
648          {
649            retainedValues.add(valueBytes);
650          }
651        }
652
653        if (retainedValues.size() == a.size())
654        {
655          additionalModifications.add(new Modification(
656               ModificationType.ADD, a.getName(), a.getRawValues()));
657        }
658        else if (! retainedValues.isEmpty())
659        {
660          additionalModifications.add(new Modification(
661               ModificationType.ADD, a.getName(),
662               StaticUtils.toArray(retainedValues, byte[].class)));
663        }
664      }
665      else
666      {
667        final Modification revertModification =
668             ModifyAuditLogMessage.getRevertModification(m);
669        if (revertModification == null)
670        {
671          throw new AuditLogException(getLogMessageLines(),
672               ERR_MODIFY_DN_MOD_NOT_REVERTIBLE.get(
673                    modifyDNChangeRecord.getDN(),
674                    m.getModificationType().getName(), m.getAttributeName()));
675        }
676        else
677        {
678          additionalModifications.add(revertModification);
679        }
680      }
681    }
682
683    final boolean revertedDeleteOldRDN;
684    if (addedNewRDNValues.size() == numNewRDNs)
685    {
686      revertedDeleteOldRDN = true;
687    }
688    else
689    {
690      revertedDeleteOldRDN = false;
691      if (! addedNewRDNValues.isEmpty())
692      {
693        for (final ObjectPair<String,byte[]> p : addedNewRDNValues)
694        {
695          additionalModifications.add(0,
696               new Modification(ModificationType.DELETE, p.getFirst(),
697                    p.getSecond()));
698        }
699      }
700    }
701
702
703    final List<LDIFChangeRecord> changeRecords = new ArrayList<>(2);
704    changeRecords.add(new LDIFModifyDNChangeRecord(revertedDN, revertedNewRDN,
705         revertedDeleteOldRDN, revertedNewSuperiorDN));
706    if (! additionalModifications.isEmpty())
707    {
708      changeRecords.add(new LDIFModifyChangeRecord(originalDN.toString(),
709           additionalModifications));
710    }
711
712    return Collections.unmodifiableList(changeRecords);
713  }
714
715
716
717  /**
718   * {@inheritDoc}
719   */
720  @Override()
721  public void toString(@NotNull final StringBuilder buffer)
722  {
723    buffer.append(getUncommentedHeaderLine());
724    buffer.append("; changeType=modify-dn; dn=\"");
725    buffer.append(modifyDNChangeRecord.getDN());
726    buffer.append("\", newRDN=\"");
727    buffer.append(modifyDNChangeRecord.getNewRDN());
728    buffer.append("\", deleteOldRDN=");
729    buffer.append(modifyDNChangeRecord.deleteOldRDN());
730
731    final String newSuperiorDN = modifyDNChangeRecord.getNewSuperiorDN();
732    if (newSuperiorDN != null)
733    {
734      buffer.append(", newSuperiorDN=\"");
735      buffer.append(newSuperiorDN);
736      buffer.append('"');
737    }
738  }
739}