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}