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}