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