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}