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