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