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.ArrayList; 026import java.util.Collections; 027import java.util.List; 028 029import com.unboundid.ldap.sdk.Attribute; 030import com.unboundid.ldap.sdk.ChangeType; 031import com.unboundid.ldap.sdk.ReadOnlyEntry; 032import com.unboundid.ldap.sdk.unboundidds.controls.UndeleteRequestControl; 033import com.unboundid.ldif.LDIFAddChangeRecord; 034import com.unboundid.ldif.LDIFChangeRecord; 035import com.unboundid.ldif.LDIFDeleteChangeRecord; 036import com.unboundid.ldif.LDIFException; 037import com.unboundid.ldif.LDIFReader; 038import com.unboundid.util.Debug; 039import com.unboundid.util.StaticUtils; 040import com.unboundid.util.ThreadSafety; 041import com.unboundid.util.ThreadSafetyLevel; 042 043import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*; 044 045 046 047/** 048 * This class provides a data structure that holds information about an audit 049 * log message that represents a delete operation. 050 * <BR> 051 * <BLOCKQUOTE> 052 * <B>NOTE:</B> This class, and other classes within the 053 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 054 * supported for use against Ping Identity, UnboundID, and 055 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 056 * for proprietary functionality or for external specifications that are not 057 * considered stable or mature enough to be guaranteed to work in an 058 * interoperable way with other types of LDAP servers. 059 * </BLOCKQUOTE> 060 */ 061@ThreadSafety(level= ThreadSafetyLevel.COMPLETELY_THREADSAFE) 062public final class DeleteAuditLogMessage 063 extends AuditLogMessage 064{ 065 /** 066 * Retrieves the serial version UID for this serializable class. 067 */ 068 private static final long serialVersionUID = 2082830761413726711L; 069 070 071 072 // Indicates whether the entry was deleted as part of a subtree delete. 073 private final Boolean deletedAsPartOfSubtreeDelete; 074 075 // Indicates whether the delete operation represents a subtree delete. 076 private final Boolean isSubtreeDelete; 077 078 // Indicates whether the delete operation represents a soft delete. 079 private final Boolean isSoftDelete; 080 081 // Indicates whether the delete operation targets a soft-deleted entry. 082 private final Boolean isSoftDeletedEntry; 083 084 // An LDIF change record that encapsulates the change represented by this 085 // delete audit log message. 086 private final LDIFDeleteChangeRecord deleteChangeRecord; 087 088 // A list of the virtual attributes from the entry that was deleted. 089 private final List<Attribute> deletedEntryVirtualAttributes; 090 091 // A read-only copy of the entry that was deleted. 092 private final ReadOnlyEntry deletedEntry; 093 094 // The resulting DN of the soft-deleted entry. 095 private final String softDeletedEntryDN; 096 097 098 099 /** 100 * Creates a new delete audit log message from the provided set of lines. 101 * 102 * @param logMessageLines The lines that comprise the log message. It must 103 * not be {@code null} or empty, and it must not 104 * contain any blank lines, although it may contain 105 * comments. In fact, it must contain at least one 106 * comment line that appears before any non-comment 107 * lines (but possibly after other comment lines) 108 * that serves as the message header. 109 * 110 * @throws AuditLogException If a problem is encountered while processing 111 * the provided list of log message lines. 112 */ 113 public DeleteAuditLogMessage(final String... logMessageLines) 114 throws AuditLogException 115 { 116 this(StaticUtils.toList(logMessageLines), logMessageLines); 117 } 118 119 120 121 /** 122 * Creates a new delete audit log message from the provided set of lines. 123 * 124 * @param logMessageLines The lines that comprise the log message. It must 125 * not be {@code null} or empty, and it must not 126 * contain any blank lines, although it may contain 127 * comments. In fact, it must contain at least one 128 * comment line that appears before any non-comment 129 * lines (but possibly after other comment lines) 130 * that serves as the message header. 131 * 132 * @throws AuditLogException If a problem is encountered while processing 133 * the provided list of log message lines. 134 */ 135 public DeleteAuditLogMessage(final List<String> logMessageLines) 136 throws AuditLogException 137 { 138 this(logMessageLines, StaticUtils.toArray(logMessageLines, String.class)); 139 } 140 141 142 143 /** 144 * Creates a new delete audit log message from the provided information. 145 * 146 * @param logMessageLineList The lines that comprise the log message as a 147 * list. 148 * @param logMessageLineArray The lines that comprise the log message as an 149 * array. 150 * 151 * @throws AuditLogException If a problem is encountered while processing 152 * the provided list of log message lines. 153 */ 154 private DeleteAuditLogMessage(final List<String> logMessageLineList, 155 final String[] logMessageLineArray) 156 throws AuditLogException 157 { 158 super(logMessageLineList); 159 160 try 161 { 162 final LDIFChangeRecord changeRecord = 163 LDIFReader.decodeChangeRecord(logMessageLineArray); 164 if (! (changeRecord instanceof LDIFDeleteChangeRecord)) 165 { 166 throw new AuditLogException(logMessageLineList, 167 ERR_DELETE_AUDIT_LOG_MESSAGE_CHANGE_TYPE_NOT_DELETE.get( 168 changeRecord.getChangeType().getName(), 169 ChangeType.DELETE.getName())); 170 } 171 172 deleteChangeRecord = (LDIFDeleteChangeRecord) changeRecord; 173 } 174 catch (final LDIFException e) 175 { 176 Debug.debugException(e); 177 throw new AuditLogException(logMessageLineList, 178 ERR_DELETE_AUDIT_LOG_MESSAGE_LINES_NOT_CHANGE_RECORD.get( 179 StaticUtils.getExceptionMessage(e)), 180 e); 181 } 182 183 deletedAsPartOfSubtreeDelete = getNamedValueAsBoolean( 184 "deletedAsPartOfSubtreeDelete", getHeaderNamedValues()); 185 isSubtreeDelete = 186 getNamedValueAsBoolean("isSubtreeDelete", getHeaderNamedValues()); 187 isSoftDelete = 188 getNamedValueAsBoolean("isSoftDelete", getHeaderNamedValues()); 189 isSoftDeletedEntry = 190 getNamedValueAsBoolean("isSoftDeletedEntry", getHeaderNamedValues()); 191 softDeletedEntryDN = getHeaderNamedValues().get("softDeletedEntryDN"); 192 deletedEntry = decodeCommentedEntry("Deleted entry real attributes", 193 logMessageLineList, deleteChangeRecord.getDN()); 194 195 final ReadOnlyEntry virtualAttributeEntry = decodeCommentedEntry( 196 "Deleted entry virtual attributes", logMessageLineList, 197 deleteChangeRecord.getDN()); 198 if (virtualAttributeEntry == null) 199 { 200 deletedEntryVirtualAttributes = null; 201 } 202 else 203 { 204 deletedEntryVirtualAttributes = Collections.unmodifiableList( 205 new ArrayList<>(virtualAttributeEntry.getAttributes())); 206 } 207 } 208 209 210 211 /** 212 * Creates a new delete audit log message from the provided set of lines. 213 * 214 * @param logMessageLines The lines that comprise the log message. It 215 * must not be {@code null} or empty, and it must 216 * not contain any blank lines, although it may 217 * contain comments. In fact, it must contain at 218 * least one comment line that appears before any 219 * non-comment lines (but possibly after other 220 * comment lines) that serves as the message 221 * header. 222 * @param deleteChangeRecord The LDIF delete change record that is described 223 * by the provided log message lines. 224 * 225 * @throws AuditLogException If a problem is encountered while processing 226 * the provided list of log message lines. 227 */ 228 DeleteAuditLogMessage(final List<String> logMessageLines, 229 final LDIFDeleteChangeRecord deleteChangeRecord) 230 throws AuditLogException 231 { 232 super(logMessageLines); 233 234 this.deleteChangeRecord = deleteChangeRecord; 235 236 deletedAsPartOfSubtreeDelete = getNamedValueAsBoolean( 237 "deletedAsPartOfSubtreeDelete", getHeaderNamedValues()); 238 isSubtreeDelete = 239 getNamedValueAsBoolean("isSubtreeDelete", getHeaderNamedValues()); 240 isSoftDelete = 241 getNamedValueAsBoolean("isSoftDelete", getHeaderNamedValues()); 242 isSoftDeletedEntry = 243 getNamedValueAsBoolean("isSoftDeletedEntry", getHeaderNamedValues()); 244 softDeletedEntryDN = getHeaderNamedValues().get("softDeletedEntryDN"); 245 deletedEntry = decodeCommentedEntry("Deleted entry real attributes", 246 logMessageLines, deleteChangeRecord.getDN()); 247 248 final ReadOnlyEntry virtualAttributeEntry = decodeCommentedEntry( 249 "Deleted entry virtual attributes", logMessageLines, 250 deleteChangeRecord.getDN()); 251 if (virtualAttributeEntry == null) 252 { 253 deletedEntryVirtualAttributes = null; 254 } 255 else 256 { 257 deletedEntryVirtualAttributes = Collections.unmodifiableList( 258 new ArrayList<>(virtualAttributeEntry.getAttributes())); 259 } 260 } 261 262 263 264 /** 265 * {@inheritDoc} 266 */ 267 @Override() 268 public String getDN() 269 { 270 return deleteChangeRecord.getDN(); 271 } 272 273 274 275 /** 276 * Retrieves the value of the flag that indicates whether this delete audit 277 * log message represents the delete of the base entry of a subtree delete 278 * operation, if available. 279 * 280 * @return {@code Boolean.TRUE} if it is known that the operation was a 281 * subtree delete, {@code Boolean.FALSE} if it is known that the 282 * operation was not a subtree delete, or {@code null} if this is not 283 * available. 284 */ 285 public Boolean getIsSubtreeDelete() 286 { 287 return isSubtreeDelete; 288 } 289 290 291 292 /** 293 * Retrieves the value of the flag that indicates whether this delete audit 294 * log record represents an entry that was deleted as part of a subtree 295 * delete (and is not the base entry for that subtree delete), if available. 296 * 297 * @return {@code Boolean.TRUE} if it is known that the entry was deleted as 298 * part of a subtree delete, {@code Boolean.FALSE} if it is known 299 * that the entry was not deleted as part of a subtree delete, or 300 * {@code null} if this is not available. 301 */ 302 public Boolean getDeletedAsPartOfSubtreeDelete() 303 { 304 return deletedAsPartOfSubtreeDelete; 305 } 306 307 308 309 /** 310 * Retrieves the value of the flag that indicates whether this delete 311 * operation was a soft delete, if available. 312 * 313 * @return {@code Boolean.TRUE} if it is known that the operation was a soft 314 * delete, {@code Boolean.FALSE} if it is known that the operation 315 * was not a soft delete, or {@code null} if this is not available. 316 */ 317 public Boolean getIsSoftDelete() 318 { 319 return isSoftDelete; 320 } 321 322 323 324 /** 325 * Retrieves the DN of the entry after it was been soft deleted, if available. 326 * 327 * @return The DN of the entry after it was soft deleted, or {@code null} if 328 * this is not available. 329 */ 330 public String getSoftDeletedEntryDN() 331 { 332 return softDeletedEntryDN; 333 } 334 335 336 337 /** 338 * Retrieves the value of the flag that indicates whether this delete 339 * operation targeted an entry that had previously been soft deleted, if 340 * available. 341 * 342 * @return {@code Boolean.TRUE} if it is known that the operation targeted a 343 * soft-deleted entry, {@code Boolean.FALSE} if it is known that the 344 * operation did not target a soft-deleted entry, or {@code null} if 345 * this is not available. 346 */ 347 public Boolean getIsSoftDeletedEntry() 348 { 349 return isSoftDeletedEntry; 350 } 351 352 353 354 /** 355 * Retrieves a read-only copy of the entry that was deleted, if available. 356 * 357 * @return A read-only copy of the entry that was deleted, or {@code null} if 358 * it is not available. 359 */ 360 public ReadOnlyEntry getDeletedEntry() 361 { 362 return deletedEntry; 363 } 364 365 366 367 /** 368 * Retrieves a list of the virtual attributes from the entry that was deleted, 369 * if available. 370 * 371 * @return A list of the virtual attributes from the entry that was deleted, 372 * or {@code null} if it is not available. 373 */ 374 public List<Attribute> getDeletedEntryVirtualAttributes() 375 { 376 return deletedEntryVirtualAttributes; 377 } 378 379 380 381 /** 382 * {@inheritDoc} 383 */ 384 @Override() 385 public ChangeType getChangeType() 386 { 387 return ChangeType.DELETE; 388 } 389 390 391 392 /** 393 * {@inheritDoc} 394 */ 395 @Override() 396 public LDIFDeleteChangeRecord getChangeRecord() 397 { 398 return deleteChangeRecord; 399 } 400 401 402 403 /** 404 * {@inheritDoc} 405 */ 406 @Override() 407 public boolean isRevertible() 408 { 409 // Subtree delete operations are not inherently revertible. The audit log 410 // should actually record a separate delete log message for each entry that 411 // was deleted as part of the subtree delete, and therefore it is possible 412 // to reverse an audit log that includes those additional delete records, 413 // but it is not possible to revert a subtree delete from a single delete 414 // audit log message. 415 // 416 // However, if this audit log message is for the base entry of a subtree 417 // delete, and if getDeletedEntry returns a non-null value, then the add 418 // change record needed to revert the delete of just that base entry can be 419 // obtained by simply creating an add change record using the entry returned 420 // by getDeletedEntry. 421 if ((isSubtreeDelete != null) && isSubtreeDelete) 422 { 423 return false; 424 } 425 426 // Non-subtree delete audit log messages are revertible under conditions: 427 // - It was a soft delete and we have the soft-deleted entry DN. 428 // - It was a hard delete and we have a copy of the entry that was deleted. 429 if ((isSoftDelete != null) && isSoftDelete) 430 { 431 return (softDeletedEntryDN != null); 432 } 433 else 434 { 435 return (deletedEntry != null); 436 } 437 } 438 439 440 441 /** 442 * {@inheritDoc} 443 */ 444 @Override() 445 public List<LDIFChangeRecord> getRevertChangeRecords() 446 throws AuditLogException 447 { 448 if ((isSubtreeDelete != null) && isSubtreeDelete) 449 { 450 if (deletedEntry == null) 451 { 452 throw new AuditLogException(getLogMessageLines(), 453 ERR_DELETE_AUDIT_LOG_MESSAGE_SUBTREE_DELETE_WITHOUT_ENTRY.get( 454 deleteChangeRecord.getDN())); 455 } 456 else 457 { 458 throw new AuditLogException(getLogMessageLines(), 459 ERR_DELETE_AUDIT_LOG_MESSAGE_SUBTREE_DELETE_WITH_ENTRY.get( 460 deleteChangeRecord.getDN())); 461 } 462 } 463 464 if ((isSoftDelete != null) && isSoftDelete) 465 { 466 if (softDeletedEntryDN != null) 467 { 468 return Collections.<LDIFChangeRecord>singletonList( 469 new LDIFAddChangeRecord( 470 UndeleteRequestControl.createUndeleteRequest( 471 deleteChangeRecord.getDN(), softDeletedEntryDN))); 472 } 473 else 474 { 475 throw new AuditLogException(getLogMessageLines(), 476 ERR_DELETE_AUDIT_LOG_MESSAGE_NO_SOFT_DELETED_ENTRY_DN.get( 477 deleteChangeRecord.getDN())); 478 } 479 } 480 else 481 { 482 if (deletedEntry != null) 483 { 484 return Collections.<LDIFChangeRecord>singletonList( 485 new LDIFAddChangeRecord(deletedEntry)); 486 } 487 else 488 { 489 throw new AuditLogException(getLogMessageLines(), 490 ERR_DELETE_AUDIT_LOG_MESSAGE_DELETED_ENTRY.get( 491 deleteChangeRecord.getDN())); 492 } 493 } 494 } 495 496 497 498 /** 499 * {@inheritDoc} 500 */ 501 @Override() 502 public void toString(final StringBuilder buffer) 503 { 504 buffer.append(getUncommentedHeaderLine()); 505 buffer.append("; changeType=delete; dn=\""); 506 buffer.append(deleteChangeRecord.getDN()); 507 buffer.append('\"'); 508 } 509}