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