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.Arrays;
041import java.util.Collections;
042import java.util.List;
043
044import com.unboundid.ldap.sdk.ChangeType;
045import com.unboundid.ldap.sdk.Modification;
046import com.unboundid.ldap.sdk.ModificationType;
047import com.unboundid.ldif.LDIFChangeRecord;
048import com.unboundid.ldif.LDIFModifyChangeRecord;
049import com.unboundid.ldif.LDIFException;
050import com.unboundid.ldif.LDIFReader;
051import com.unboundid.util.Debug;
052import com.unboundid.util.NotNull;
053import com.unboundid.util.Nullable;
054import com.unboundid.util.StaticUtils;
055import com.unboundid.util.ThreadSafety;
056import com.unboundid.util.ThreadSafetyLevel;
057
058import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*;
059
060
061
062/**
063 * This class provides a data structure that holds information about an audit
064 * log message that represents a modify operation.
065 * <BR>
066 * <BLOCKQUOTE>
067 *   <B>NOTE:</B>  This class, and other classes within the
068 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
069 *   supported for use against Ping Identity, UnboundID, and
070 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
071 *   for proprietary functionality or for external specifications that are not
072 *   considered stable or mature enough to be guaranteed to work in an
073 *   interoperable way with other types of LDAP servers.
074 * </BLOCKQUOTE>
075 */
076@ThreadSafety(level= ThreadSafetyLevel.COMPLETELY_THREADSAFE)
077public final class ModifyAuditLogMessage
078       extends AuditLogMessage
079{
080  /**
081   * Retrieves the serial version UID for this serializable class.
082   */
083  private static final long serialVersionUID = -5262466264778465574L;
084
085
086
087  // Indicates whether the modify operation targets a soft-deleted entry.
088  @Nullable private final Boolean isSoftDeletedEntry;
089
090  // An LDIF change record that encapsulates the change represented by this
091  // modify audit log message.
092  @NotNull private final LDIFModifyChangeRecord modifyChangeRecord;
093
094
095
096  /**
097   * Creates a new modify audit log message from the provided set of lines.
098   *
099   * @param  logMessageLines  The lines that comprise the log message.  It must
100   *                          not be {@code null} or empty, and it must not
101   *                          contain any blank lines, although it may contain
102   *                          comments.  In fact, it must contain at least one
103   *                          comment line that appears before any non-comment
104   *                          lines (but possibly after other comment line) that
105   *                          serves as the message header.
106   *
107   * @throws  AuditLogException  If a problem is encountered while processing
108   *                             the provided list of log message lines.
109   */
110  public ModifyAuditLogMessage(@NotNull final String... logMessageLines)
111         throws AuditLogException
112  {
113    this(StaticUtils.toList(logMessageLines), logMessageLines);
114  }
115
116
117
118  /**
119   * Creates a new modify audit log message from the provided set of lines.
120   *
121   * @param  logMessageLines  The lines that comprise the log message.  It must
122   *                          not be {@code null} or empty, and it must not
123   *                          contain any blank lines, although it may contain
124   *                          comments.  In fact, it must contain at least one
125   *                          comment line that appears before any non-comment
126   *                          lines (but possibly after other comment line) that
127   *                          serves as the message header.
128   *
129   * @throws  AuditLogException  If a problem is encountered while processing
130   *                             the provided list of log message lines.
131   */
132  public ModifyAuditLogMessage(@NotNull final List<String> logMessageLines)
133         throws AuditLogException
134  {
135    this(logMessageLines, StaticUtils.toArray(logMessageLines, String.class));
136  }
137
138
139
140  /**
141   * Creates a new modify audit log message from the provided information.
142   *
143   * @param  logMessageLineList   The lines that comprise the log message as a
144   *                              list.
145   * @param  logMessageLineArray  The lines that comprise the log message as an
146   *                              array.
147   *
148   * @throws  AuditLogException  If a problem is encountered while processing
149   *                             the provided list of log message lines.
150   */
151  private ModifyAuditLogMessage(@NotNull final List<String> logMessageLineList,
152                                @NotNull final String[] logMessageLineArray)
153          throws AuditLogException
154  {
155    super(logMessageLineList);
156
157    try
158    {
159      final LDIFChangeRecord changeRecord =
160           LDIFReader.decodeChangeRecord(logMessageLineArray);
161      if (! (changeRecord instanceof LDIFModifyChangeRecord))
162      {
163        throw new AuditLogException(logMessageLineList,
164             ERR_MODIFY_AUDIT_LOG_MESSAGE_CHANGE_TYPE_NOT_MODIFY.get(
165                  changeRecord.getChangeType().getName(),
166                  ChangeType.MODIFY.getName()));
167      }
168
169      modifyChangeRecord = (LDIFModifyChangeRecord) changeRecord;
170    }
171    catch (final LDIFException e)
172    {
173      Debug.debugException(e);
174      throw new AuditLogException(logMessageLineList,
175           ERR_MODIFY_AUDIT_LOG_MESSAGE_LINES_NOT_CHANGE_RECORD.get(
176                StaticUtils.getExceptionMessage(e)),
177           e);
178    }
179
180    isSoftDeletedEntry =
181         getNamedValueAsBoolean("isSoftDeletedEntry", getHeaderNamedValues());
182  }
183
184
185
186  /**
187   * Creates a new modify audit log message from the provided set of lines.
188   *
189   * @param  logMessageLines     The lines that comprise the log message.  It
190   *                             must not be {@code null} or empty, and it must
191   *                             not contain any blank lines, although it may
192   *                             contain comments.  In fact, it must contain at
193   *                             least one comment line that appears before any
194   *                             non-comment lines (but possibly after other
195   *                             comment line) that serves as the message
196   *                             header.
197   * @param  modifyChangeRecord  The LDIF modify change record that is described
198   *                             by the provided log message lines.
199   *
200   * @throws  AuditLogException  If a problem is encountered while processing
201   *                             the provided list of log message lines.
202   */
203  ModifyAuditLogMessage(@NotNull final List<String> logMessageLines,
204       @NotNull final LDIFModifyChangeRecord modifyChangeRecord)
205       throws AuditLogException
206  {
207    super(logMessageLines);
208
209    this.modifyChangeRecord = modifyChangeRecord;
210
211    isSoftDeletedEntry =
212         getNamedValueAsBoolean("isSoftDeletedEntry", getHeaderNamedValues());
213  }
214
215
216
217  /**
218   * {@inheritDoc}
219   */
220  @Override()
221  @NotNull()
222  public String getDN()
223  {
224    return modifyChangeRecord.getDN();
225  }
226
227
228
229  /**
230   * Retrieves a list of the modifications included in the associated modify
231   * operation.
232   *
233   * @return  A list of the modifications included in the associated modify
234   *          operation.
235   */
236  @NotNull()
237  public List<Modification> getModifications()
238  {
239    return Collections.unmodifiableList(
240         Arrays.asList(modifyChangeRecord.getModifications()));
241  }
242
243
244
245  /**
246   * Retrieves the value of the flag that indicates whether this modify
247   * operation targeted an entry that had previously been soft deleted, if
248   * available.
249   *
250   * @return  {@code Boolean.TRUE} if it is known that the operation targeted a
251   *          soft-deleted entry, {@code Boolean.FALSE} if it is known that the
252   *          operation did not target a soft-deleted entry, or {@code null} if
253   *          this is not available.
254   */
255@Nullable   public Boolean getIsSoftDeletedEntry()
256  {
257    return isSoftDeletedEntry;
258  }
259
260
261
262  /**
263   * {@inheritDoc}
264   */
265  @Override()
266  @NotNull()
267  public ChangeType getChangeType()
268  {
269    return ChangeType.MODIFY;
270  }
271
272
273
274  /**
275   * {@inheritDoc}
276   */
277  @Override()
278  @NotNull()
279  public LDIFModifyChangeRecord getChangeRecord()
280  {
281    return modifyChangeRecord;
282  }
283
284
285
286  /**
287   * {@inheritDoc}
288   */
289  @Override()
290  public boolean isRevertible()
291  {
292    // Modify audit log messages are revertible as long as both of the following
293    // are true:
294    // - It must not contain any REPLACE modifications, with or without values.
295    // - It must not contain any DELETE modifications without values.  DELETE
296    //   modifications with values are fine.
297    for (final Modification m : modifyChangeRecord.getModifications())
298    {
299      if (! modificationIsRevertible(m))
300      {
301        return false;
302      }
303    }
304
305    // If we've gotten here, then it must be acceptable.
306    return true;
307  }
308
309
310
311  /**
312   * Indicates whether the provided modification is revertible.
313   *
314   * @param  m  The modification for which to make the determination.  It must
315   *            not be {@code null}.
316   *
317   * @return  {@code true} if the modification is revertible, or {@code false}
318   *          if not.
319   */
320  static boolean modificationIsRevertible(@NotNull final Modification m)
321  {
322    switch (m.getModificationType().intValue())
323    {
324      case ModificationType.ADD_INT_VALUE:
325      case ModificationType.INCREMENT_INT_VALUE:
326        // This is always revertible.
327        return true;
328
329      case ModificationType.DELETE_INT_VALUE:
330        // This is revertible as long as it has one or more values.
331        return m.hasValue();
332
333      case ModificationType.REPLACE_INT_VALUE:
334      default:
335        // This is never revertible.
336        return false;
337    }
338  }
339
340
341
342  /**
343   * Retrieves a modification that can be used to revert the provided
344   * modification.
345   *
346   * @param  m  The modification for which to retrieve the revert modification.
347   *            It must not be {@code null}.
348   *
349   * @return  A modification that can be used to revert the provided
350   *          modification, or {@code null} if the provided modification cannot
351   *          be reverted.
352   */
353  @Nullable()
354  static Modification getRevertModification(@NotNull final Modification m)
355  {
356    switch (m.getModificationType().intValue())
357    {
358      case ModificationType.ADD_INT_VALUE:
359        return new Modification(ModificationType.DELETE, m.getAttributeName(),
360             m.getRawValues());
361
362      case ModificationType.INCREMENT_INT_VALUE:
363        final String firstValue = m.getValues()[0];
364        if (firstValue.startsWith("-"))
365        {
366          return new Modification(ModificationType.INCREMENT,
367               m.getAttributeName(), firstValue.substring(1));
368        }
369        else
370        {
371          return new Modification(ModificationType.INCREMENT,
372               m.getAttributeName(), '-' + firstValue);
373        }
374
375      case ModificationType.DELETE_INT_VALUE:
376        if (m.hasValue())
377        {
378          return new Modification(ModificationType.ADD, m.getAttributeName(),
379               m.getRawValues());
380        }
381        else
382        {
383          return null;
384        }
385
386      case ModificationType.REPLACE_INT_VALUE:
387      default:
388        return null;
389    }
390  }
391
392
393
394  /**
395   * {@inheritDoc}
396   */
397  @Override()
398  @NotNull()
399  public List<LDIFChangeRecord> getRevertChangeRecords()
400         throws AuditLogException
401  {
402    // Iterate through the modifications backwards and construct the
403    // appropriate set of modifications to revert each of them.
404    final Modification[] mods = modifyChangeRecord.getModifications();
405    final Modification[] revertMods = new Modification[mods.length];
406    for (int i=mods.length - 1, j = 0; i >= 0; i--, j++)
407    {
408      revertMods[j] = getRevertModification(mods[i]);
409      if (revertMods[j] == null)
410      {
411        throw new AuditLogException(getLogMessageLines(),
412             ERR_MODIFY_AUDIT_LOG_MESSAGE_MOD_NOT_REVERTIBLE.get(
413                  modifyChangeRecord.getDN(), String.valueOf(mods[i])));
414      }
415    }
416
417    return Collections.<LDIFChangeRecord>singletonList(
418         new LDIFModifyChangeRecord(modifyChangeRecord.getDN(), revertMods));
419  }
420
421
422
423  /**
424   * {@inheritDoc}
425   */
426  @Override()
427  public void toString(@NotNull final StringBuilder buffer)
428  {
429    buffer.append(getUncommentedHeaderLine());
430    buffer.append("; changeType=modify; dn=\"");
431    buffer.append(modifyChangeRecord.getDN());
432    buffer.append('\"');
433  }
434}