001/* 002 * Copyright 2007-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2007-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) 2007-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; 037 038 039 040import java.nio.charset.StandardCharsets; 041import java.util.ArrayList; 042import java.util.Arrays; 043import java.util.Collections; 044import java.util.List; 045import java.util.StringTokenizer; 046 047import com.unboundid.ldif.LDIFAddChangeRecord; 048import com.unboundid.ldif.LDIFChangeRecord; 049import com.unboundid.ldif.LDIFDeleteChangeRecord; 050import com.unboundid.ldif.LDIFException; 051import com.unboundid.ldif.LDIFModifyChangeRecord; 052import com.unboundid.ldif.LDIFModifyDNChangeRecord; 053import com.unboundid.ldif.LDIFReader; 054import com.unboundid.ldif.TrailingSpaceBehavior; 055import com.unboundid.ldap.matchingrules.BooleanMatchingRule; 056import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule; 057import com.unboundid.ldap.matchingrules.IntegerMatchingRule; 058import com.unboundid.ldap.matchingrules.OctetStringMatchingRule; 059import com.unboundid.util.Debug; 060import com.unboundid.util.NotExtensible; 061import com.unboundid.util.NotMutable; 062import com.unboundid.util.NotNull; 063import com.unboundid.util.Nullable; 064import com.unboundid.util.StaticUtils; 065import com.unboundid.util.ThreadSafety; 066import com.unboundid.util.ThreadSafetyLevel; 067 068import static com.unboundid.ldap.sdk.LDAPMessages.*; 069 070 071 072/** 073 * This class provides a data structure for representing a changelog entry as 074 * described in draft-good-ldap-changelog. Changelog entries provide 075 * information about a change (add, delete, modify, or modify DN) operation 076 * that was processed in the directory server. Changelog entries may be 077 * parsed from entries, and they may be converted to LDIF change records or 078 * processed as LDAP operations. 079 */ 080@NotExtensible() 081@NotMutable() 082@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 083public class ChangeLogEntry 084 extends ReadOnlyEntry 085{ 086 /** 087 * The name of the attribute that contains the change number that identifies 088 * the change and the order it was processed in the server. 089 */ 090 @NotNull public static final String ATTR_CHANGE_NUMBER = "changeNumber"; 091 092 093 094 /** 095 * The name of the attribute that contains the DN of the entry targeted by 096 * the change. 097 */ 098 @NotNull public static final String ATTR_TARGET_DN = "targetDN"; 099 100 101 102 /** 103 * The name of the attribute that contains the type of change made to the 104 * target entry. 105 */ 106 @NotNull public static final String ATTR_CHANGE_TYPE = "changeType"; 107 108 109 110 /** 111 * The name of the attribute used to hold a list of changes. For an add 112 * operation, this will be an LDIF representation of the attributes that make 113 * up the entry. For a modify operation, this will be an LDIF representation 114 * of the changes to the target entry. 115 */ 116 @NotNull public static final String ATTR_CHANGES = "changes"; 117 118 119 120 /** 121 * The name of the attribute used to hold the new RDN for a modify DN 122 * operation. 123 */ 124 @NotNull public static final String ATTR_NEW_RDN = "newRDN"; 125 126 127 128 /** 129 * The name of the attribute used to hold the flag indicating whether the old 130 * RDN value(s) should be removed from the target entry for a modify DN 131 * operation. 132 */ 133 @NotNull public static final String ATTR_DELETE_OLD_RDN = "deleteOldRDN"; 134 135 136 137 /** 138 * The name of the attribute used to hold the new superior DN for a modify DN 139 * operation. 140 */ 141 @NotNull public static final String ATTR_NEW_SUPERIOR = "newSuperior"; 142 143 144 145 /** 146 * The name of the attribute used to hold information about attributes from a 147 * deleted entry, if available. 148 */ 149 @NotNull public static final String ATTR_DELETED_ENTRY_ATTRS = 150 "deletedEntryAttrs"; 151 152 153 154 /** 155 * The name of an alternative attribute that may be used to obtain information 156 * about attributes from a deleted entry if the deletedEntryAttrs attribute 157 * is not present. 158 */ 159 @NotNull public static final String 160 ATTR_ALTERNATIVE_DELETED_ENTRY_ATTRS_INCLUDED_ATTRIBUTES = 161 "includedAttributes"; 162 163 164 165 /** 166 * The serial version UID for this serializable class. 167 */ 168 private static final long serialVersionUID = -4018129098468341663L; 169 170 171 172 // Indicates whether to delete the old RDN value(s) in a modify DN operation. 173 private final boolean deleteOldRDN; 174 175 // The change type for this changelog entry. 176 @NotNull private final ChangeType changeType; 177 178 // A list of the attributes for an add, or the deleted entry attributes for a 179 // delete operation. 180 @Nullable private final List<Attribute> attributes; 181 182 // A list of the modifications for a modify operation. 183 @Nullable private final List<Modification> modifications; 184 185 // The change number for the changelog entry. 186 private final long changeNumber; 187 188 // The new RDN for a modify DN operation. 189 @Nullable private final String newRDN; 190 191 // The new superior DN for a modify DN operation. 192 @Nullable private final String newSuperior; 193 194 // The DN of the target entry. 195 @NotNull private final String targetDN; 196 197 198 199 /** 200 * Creates a new changelog entry from the provided entry. 201 * 202 * @param entry The entry from which to create this changelog entry. 203 * 204 * @throws LDAPException If the provided entry cannot be parsed as a 205 * changelog entry. 206 */ 207 public ChangeLogEntry(@NotNull final Entry entry) 208 throws LDAPException 209 { 210 super(entry); 211 212 213 final Attribute changeNumberAttr = entry.getAttribute(ATTR_CHANGE_NUMBER); 214 if ((changeNumberAttr == null) || (! changeNumberAttr.hasValue())) 215 { 216 throw new LDAPException(ResultCode.DECODING_ERROR, 217 ERR_CHANGELOG_NO_CHANGE_NUMBER.get()); 218 } 219 220 try 221 { 222 changeNumber = Long.parseLong(changeNumberAttr.getValue()); 223 } 224 catch (final NumberFormatException nfe) 225 { 226 Debug.debugException(nfe); 227 throw new LDAPException(ResultCode.DECODING_ERROR, 228 ERR_CHANGELOG_INVALID_CHANGE_NUMBER.get(changeNumberAttr.getValue()), 229 nfe); 230 } 231 232 233 final Attribute targetDNAttr = entry.getAttribute(ATTR_TARGET_DN); 234 if ((targetDNAttr == null) || (! targetDNAttr.hasValue())) 235 { 236 throw new LDAPException(ResultCode.DECODING_ERROR, 237 ERR_CHANGELOG_NO_TARGET_DN.get()); 238 } 239 targetDN = targetDNAttr.getValue(); 240 241 242 final Attribute changeTypeAttr = entry.getAttribute(ATTR_CHANGE_TYPE); 243 if ((changeTypeAttr == null) || (! changeTypeAttr.hasValue())) 244 { 245 throw new LDAPException(ResultCode.DECODING_ERROR, 246 ERR_CHANGELOG_NO_CHANGE_TYPE.get()); 247 } 248 changeType = ChangeType.forName(changeTypeAttr.getValue()); 249 if (changeType == null) 250 { 251 throw new LDAPException(ResultCode.DECODING_ERROR, 252 ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue())); 253 } 254 255 256 switch (changeType) 257 { 258 case ADD: 259 attributes = parseAddAttributeList(entry, ATTR_CHANGES, targetDN); 260 modifications = null; 261 newRDN = null; 262 deleteOldRDN = false; 263 newSuperior = null; 264 break; 265 266 case DELETE: 267 attributes = parseDeletedAttributeList(entry, targetDN); 268 modifications = null; 269 newRDN = null; 270 deleteOldRDN = false; 271 newSuperior = null; 272 break; 273 274 case MODIFY: 275 attributes = null; 276 modifications = parseModificationList(entry, targetDN); 277 newRDN = null; 278 deleteOldRDN = false; 279 newSuperior = null; 280 break; 281 282 case MODIFY_DN: 283 attributes = null; 284 modifications = parseModificationList(entry, targetDN); 285 newSuperior = getAttributeValue(ATTR_NEW_SUPERIOR); 286 287 final Attribute newRDNAttr = getAttribute(ATTR_NEW_RDN); 288 if ((newRDNAttr == null) || (! newRDNAttr.hasValue())) 289 { 290 throw new LDAPException(ResultCode.DECODING_ERROR, 291 ERR_CHANGELOG_MISSING_NEW_RDN.get()); 292 } 293 newRDN = newRDNAttr.getValue(); 294 295 final Attribute deleteOldRDNAttr = getAttribute(ATTR_DELETE_OLD_RDN); 296 if ((deleteOldRDNAttr == null) || (! deleteOldRDNAttr.hasValue())) 297 { 298 throw new LDAPException(ResultCode.DECODING_ERROR, 299 ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get()); 300 } 301 final String delOldRDNStr = 302 StaticUtils.toLowerCase(deleteOldRDNAttr.getValue()); 303 if (delOldRDNStr.equals("true")) 304 { 305 deleteOldRDN = true; 306 } 307 else if (delOldRDNStr.equals("false")) 308 { 309 deleteOldRDN = false; 310 } 311 else 312 { 313 throw new LDAPException(ResultCode.DECODING_ERROR, 314 ERR_CHANGELOG_MISSING_DELETE_OLD_RDN.get(delOldRDNStr)); 315 } 316 break; 317 318 default: 319 // This should never happen. 320 throw new LDAPException(ResultCode.DECODING_ERROR, 321 ERR_CHANGELOG_INVALID_CHANGE_TYPE.get(changeTypeAttr.getValue())); 322 } 323 } 324 325 326 327 /** 328 * Constructs a changelog entry from information contained in the provided 329 * LDIF change record. 330 * 331 * @param changeNumber The change number to use for the constructed 332 * changelog entry. 333 * @param changeRecord The LDIF change record with the information to 334 * include in the generated changelog entry. 335 * 336 * @return The changelog entry constructed from the provided change record. 337 * 338 * @throws LDAPException If a problem is encountered while constructing the 339 * changelog entry. 340 */ 341 @NotNull() 342 public static ChangeLogEntry constructChangeLogEntry(final long changeNumber, 343 @NotNull final LDIFChangeRecord changeRecord) 344 throws LDAPException 345 { 346 final Entry e = 347 new Entry(ATTR_CHANGE_NUMBER + '=' + changeNumber + ",cn=changelog"); 348 e.addAttribute("objectClass", "top", "changeLogEntry"); 349 e.addAttribute(new Attribute(ATTR_CHANGE_NUMBER, 350 IntegerMatchingRule.getInstance(), String.valueOf(changeNumber))); 351 e.addAttribute(new Attribute(ATTR_TARGET_DN, 352 DistinguishedNameMatchingRule.getInstance(), changeRecord.getDN())); 353 e.addAttribute(ATTR_CHANGE_TYPE, changeRecord.getChangeType().getName()); 354 355 switch (changeRecord.getChangeType()) 356 { 357 case ADD: 358 // The changes attribute should be an LDIF-encoded representation of the 359 // attributes from the entry, which is the LDIF representation of the 360 // entry without the first line (which contains the DN). 361 final LDIFAddChangeRecord addRecord = 362 (LDIFAddChangeRecord) changeRecord; 363 final Entry addEntry = new Entry(addRecord.getDN(), 364 addRecord.getAttributes()); 365 final String[] entryLdifLines = addEntry.toLDIF(0); 366 final StringBuilder entryLDIFBuffer = new StringBuilder(); 367 for (int i=1; i < entryLdifLines.length; i++) 368 { 369 entryLDIFBuffer.append(entryLdifLines[i]); 370 entryLDIFBuffer.append(StaticUtils.EOL); 371 } 372 e.addAttribute(new Attribute(ATTR_CHANGES, 373 OctetStringMatchingRule.getInstance(), 374 entryLDIFBuffer.toString())); 375 break; 376 377 case DELETE: 378 // No additional information is needed. 379 break; 380 381 case MODIFY: 382 // The changes attribute should be an LDIF-encoded representation of the 383 // modification, with the first two lines (the DN and changetype) 384 // removed. 385 final String[] modLdifLines = changeRecord.toLDIF(0); 386 final StringBuilder modLDIFBuffer = new StringBuilder(); 387 for (int i=2; i < modLdifLines.length; i++) 388 { 389 modLDIFBuffer.append(modLdifLines[i]); 390 modLDIFBuffer.append(StaticUtils.EOL); 391 } 392 e.addAttribute(new Attribute(ATTR_CHANGES, 393 OctetStringMatchingRule.getInstance(), modLDIFBuffer.toString())); 394 break; 395 396 case MODIFY_DN: 397 final LDIFModifyDNChangeRecord modDNRecord = 398 (LDIFModifyDNChangeRecord) changeRecord; 399 e.addAttribute(new Attribute(ATTR_NEW_RDN, 400 DistinguishedNameMatchingRule.getInstance(), 401 modDNRecord.getNewRDN())); 402 e.addAttribute(new Attribute(ATTR_DELETE_OLD_RDN, 403 BooleanMatchingRule.getInstance(), 404 (modDNRecord.deleteOldRDN() ? "TRUE" : "FALSE"))); 405 if (modDNRecord.getNewSuperiorDN() != null) 406 { 407 e.addAttribute(new Attribute(ATTR_NEW_SUPERIOR, 408 DistinguishedNameMatchingRule.getInstance(), 409 modDNRecord.getNewSuperiorDN())); 410 } 411 break; 412 } 413 414 return new ChangeLogEntry(e); 415 } 416 417 418 419 /** 420 * Parses the attribute list from the specified attribute in a changelog 421 * entry. 422 * 423 * @param entry The entry containing the data to parse. 424 * @param attrName The name of the attribute from which to parse the 425 * attribute list. 426 * @param targetDN The DN of the target entry. 427 * 428 * @return The parsed attribute list. 429 * 430 * @throws LDAPException If an error occurs while parsing the attribute 431 * list. 432 */ 433 @NotNull() 434 protected static List<Attribute> parseAddAttributeList( 435 @NotNull final Entry entry, 436 @NotNull final String attrName, 437 @NotNull final String targetDN) 438 throws LDAPException 439 { 440 final Attribute changesAttr = entry.getAttribute(attrName); 441 if ((changesAttr == null) || (! changesAttr.hasValue())) 442 { 443 throw new LDAPException(ResultCode.DECODING_ERROR, 444 ERR_CHANGELOG_MISSING_CHANGES.get()); 445 } 446 447 final ArrayList<String> ldifLines = new ArrayList<>(20); 448 ldifLines.add("dn: " + targetDN); 449 450 final StringTokenizer tokenizer = 451 new StringTokenizer(changesAttr.getValue(), "\r\n"); 452 while (tokenizer.hasMoreTokens()) 453 { 454 ldifLines.add(tokenizer.nextToken()); 455 } 456 457 final String[] lineArray = new String[ldifLines.size()]; 458 ldifLines.toArray(lineArray); 459 460 try 461 { 462 final Entry e = LDIFReader.decodeEntry(true, TrailingSpaceBehavior.RETAIN, 463 null, lineArray); 464 return Collections.unmodifiableList(new ArrayList<>(e.getAttributes())); 465 } 466 catch (final LDIFException le) 467 { 468 Debug.debugException(le); 469 throw new LDAPException(ResultCode.DECODING_ERROR, 470 ERR_CHANGELOG_CANNOT_PARSE_ATTR_LIST.get(attrName, 471 StaticUtils.getExceptionMessage(le)), 472 le); 473 } 474 } 475 476 477 478 /** 479 * Parses the list of deleted attributes from a changelog entry representing a 480 * delete operation. The attribute is optional, so it may not be present at 481 * all, and there are two different encodings that we need to handle. One 482 * encoding is the same as is used for the add attribute list, and the second 483 * is similar to the encoding used for the list of changes, except that it 484 * ends with a NULL byte (0x00). 485 * 486 * @param entry The entry containing the data to parse. 487 * @param targetDN The DN of the target entry. 488 * 489 * @return The parsed deleted attribute list, or {@code null} if the 490 * changelog entry does not include a deleted attribute list. 491 * 492 * @throws LDAPException If an error occurs while parsing the deleted 493 * attribute list. 494 */ 495 @Nullable() 496 private static List<Attribute> parseDeletedAttributeList( 497 @NotNull final Entry entry, 498 @NotNull final String targetDN) 499 throws LDAPException 500 { 501 Attribute deletedEntryAttrs = 502 entry.getAttribute(ATTR_DELETED_ENTRY_ATTRS); 503 if ((deletedEntryAttrs == null) || (! deletedEntryAttrs.hasValue())) 504 { 505 deletedEntryAttrs = entry.getAttribute( 506 ATTR_ALTERNATIVE_DELETED_ENTRY_ATTRS_INCLUDED_ATTRIBUTES); 507 if ((deletedEntryAttrs == null) || (! deletedEntryAttrs.hasValue())) 508 { 509 return null; 510 } 511 } 512 513 final byte[] valueBytes = deletedEntryAttrs.getValueByteArray(); 514 if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00)) 515 { 516 final String valueStr = new String(valueBytes, 0, valueBytes.length-2, 517 StandardCharsets.UTF_8); 518 519 final ArrayList<String> ldifLines = new ArrayList<>(20); 520 ldifLines.add("dn: " + targetDN); 521 ldifLines.add("changetype: modify"); 522 523 final StringTokenizer tokenizer = new StringTokenizer(valueStr, "\r\n"); 524 while (tokenizer.hasMoreTokens()) 525 { 526 ldifLines.add(tokenizer.nextToken()); 527 } 528 529 final String[] lineArray = new String[ldifLines.size()]; 530 ldifLines.toArray(lineArray); 531 532 try 533 { 534 535 final LDIFModifyChangeRecord changeRecord = 536 (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray); 537 final Modification[] mods = changeRecord.getModifications(); 538 final ArrayList<Attribute> attrs = new ArrayList<>(mods.length); 539 for (final Modification m : mods) 540 { 541 if (! m.getModificationType().equals(ModificationType.DELETE)) 542 { 543 throw new LDAPException(ResultCode.DECODING_ERROR, 544 ERR_CHANGELOG_INVALID_DELENTRYATTRS_MOD_TYPE.get( 545 ATTR_DELETED_ENTRY_ATTRS)); 546 } 547 548 attrs.add(m.getAttribute()); 549 } 550 551 return Collections.unmodifiableList(attrs); 552 } 553 catch (final LDIFException le) 554 { 555 Debug.debugException(le); 556 throw new LDAPException(ResultCode.DECODING_ERROR, 557 ERR_CHANGELOG_INVALID_DELENTRYATTRS_MODS.get( 558 ATTR_DELETED_ENTRY_ATTRS, 559 StaticUtils.getExceptionMessage(le)), 560 le); 561 } 562 } 563 else 564 { 565 final ArrayList<String> ldifLines = new ArrayList<>(20); 566 ldifLines.add("dn: " + targetDN); 567 568 final StringTokenizer tokenizer = 569 new StringTokenizer(deletedEntryAttrs.getValue(), "\r\n"); 570 while (tokenizer.hasMoreTokens()) 571 { 572 ldifLines.add(tokenizer.nextToken()); 573 } 574 575 final String[] lineArray = new String[ldifLines.size()]; 576 ldifLines.toArray(lineArray); 577 578 try 579 { 580 final Entry e = LDIFReader.decodeEntry(true, 581 TrailingSpaceBehavior.RETAIN, null, lineArray); 582 return Collections.unmodifiableList(new ArrayList<>(e.getAttributes())); 583 } 584 catch (final LDIFException le) 585 { 586 Debug.debugException(le); 587 throw new LDAPException(ResultCode.DECODING_ERROR, 588 ERR_CHANGELOG_CANNOT_PARSE_DELENTRYATTRS.get( 589 ATTR_DELETED_ENTRY_ATTRS, 590 StaticUtils.getExceptionMessage(le)), 591 le); 592 } 593 } 594 } 595 596 597 598 /** 599 * Parses the modification list from a changelog entry representing a modify 600 * operation. 601 * 602 * @param entry The entry containing the data to parse. 603 * @param targetDN The DN of the target entry. 604 * 605 * @return The parsed modification list, or {@code null} if the changelog 606 * entry does not include any modifications. 607 * 608 * @throws LDAPException If an error occurs while parsing the modification 609 * list. 610 */ 611 @Nullable() 612 private static List<Modification> parseModificationList( 613 @NotNull final Entry entry, 614 @NotNull final String targetDN) 615 throws LDAPException 616 { 617 final Attribute changesAttr = entry.getAttribute(ATTR_CHANGES); 618 if ((changesAttr == null) || (! changesAttr.hasValue())) 619 { 620 return null; 621 } 622 623 final byte[] valueBytes = changesAttr.getValueByteArray(); 624 if (valueBytes.length == 0) 625 { 626 return null; 627 } 628 629 630 final ArrayList<String> ldifLines = new ArrayList<>(20); 631 ldifLines.add("dn: " + targetDN); 632 ldifLines.add("changetype: modify"); 633 634 // Even though it's a violation of the specification in 635 // draft-good-ldap-changelog, it appears that some servers (e.g., Sun DSEE) 636 // may terminate the changes value with a null character (\u0000). If that 637 // is the case, then we'll need to strip it off before trying to parse it. 638 final StringTokenizer tokenizer; 639 if ((valueBytes.length > 0) && (valueBytes[valueBytes.length-1] == 0x00)) 640 { 641 final String fullValue = changesAttr.getValue(); 642 final String realValue = fullValue.substring(0, fullValue.length()-2); 643 tokenizer = new StringTokenizer(realValue, "\r\n"); 644 } 645 else 646 { 647 tokenizer = new StringTokenizer(changesAttr.getValue(), "\r\n"); 648 } 649 650 while (tokenizer.hasMoreTokens()) 651 { 652 ldifLines.add(tokenizer.nextToken()); 653 } 654 655 final String[] lineArray = new String[ldifLines.size()]; 656 ldifLines.toArray(lineArray); 657 658 try 659 { 660 final LDIFModifyChangeRecord changeRecord = 661 (LDIFModifyChangeRecord) LDIFReader.decodeChangeRecord(lineArray); 662 return Collections.unmodifiableList( 663 Arrays.asList(changeRecord.getModifications())); 664 } 665 catch (final LDIFException le) 666 { 667 Debug.debugException(le); 668 throw new LDAPException(ResultCode.DECODING_ERROR, 669 ERR_CHANGELOG_CANNOT_PARSE_MOD_LIST.get(ATTR_CHANGES, 670 StaticUtils.getExceptionMessage(le)), 671 le); 672 } 673 } 674 675 676 677 /** 678 * Retrieves the change number for this changelog entry. 679 * 680 * @return The change number for this changelog entry. 681 */ 682 public final long getChangeNumber() 683 { 684 return changeNumber; 685 } 686 687 688 689 /** 690 * Retrieves the target DN for this changelog entry. 691 * 692 * @return The target DN for this changelog entry. 693 */ 694 @NotNull() 695 public final String getTargetDN() 696 { 697 return targetDN; 698 } 699 700 701 702 /** 703 * Retrieves the change type for this changelog entry. 704 * 705 * @return The change type for this changelog entry. 706 */ 707 @NotNull() 708 public final ChangeType getChangeType() 709 { 710 return changeType; 711 } 712 713 714 715 /** 716 * Retrieves the attribute list for an add changelog entry. 717 * 718 * @return The attribute list for an add changelog entry, or {@code null} if 719 * this changelog entry does not represent an add operation. 720 */ 721 @Nullable() 722 public final List<Attribute> getAddAttributes() 723 { 724 if (changeType == ChangeType.ADD) 725 { 726 return attributes; 727 } 728 else 729 { 730 return null; 731 } 732 } 733 734 735 736 /** 737 * Retrieves the list of deleted entry attributes for a delete changelog 738 * entry. Note that this is a non-standard extension implemented by some 739 * types of servers and is not defined in draft-good-ldap-changelog and may 740 * not be provided by some servers. 741 * 742 * @return The delete entry attribute list for a delete changelog entry, or 743 * {@code null} if this changelog entry does not represent a delete 744 * operation or no deleted entry attributes were included in the 745 * changelog entry. 746 */ 747 @Nullable() 748 public final List<Attribute> getDeletedEntryAttributes() 749 { 750 if (changeType == ChangeType.DELETE) 751 { 752 return attributes; 753 } 754 else 755 { 756 return null; 757 } 758 } 759 760 761 762 /** 763 * Retrieves the list of modifications for a modify changelog entry. Note 764 * some directory servers may also include changes for modify DN change 765 * records if there were updates to operational attributes (e.g., 766 * modifiersName and modifyTimestamp). 767 * 768 * @return The list of modifications for a modify (or possibly modify DN) 769 * changelog entry, or {@code null} if this changelog entry does 770 * not represent a modify operation or a modify DN operation with 771 * additional changes. 772 */ 773 @Nullable 774 public final List<Modification> getModifications() 775 { 776 return modifications; 777 } 778 779 780 781 /** 782 * Retrieves the new RDN for a modify DN changelog entry. 783 * 784 * @return The new RDN for a modify DN changelog entry, or {@code null} if 785 * this changelog entry does not represent a modify DN operation. 786 */ 787 @Nullable() 788 public final String getNewRDN() 789 { 790 return newRDN; 791 } 792 793 794 795 /** 796 * Indicates whether the old RDN value(s) should be removed from the entry 797 * targeted by this modify DN changelog entry. 798 * 799 * @return {@code true} if the old RDN value(s) should be removed from the 800 * entry, or {@code false} if not or if this changelog entry does not 801 * represent a modify DN operation. 802 */ 803 public final boolean deleteOldRDN() 804 { 805 return deleteOldRDN; 806 } 807 808 809 810 /** 811 * Retrieves the new superior DN for a modify DN changelog entry. 812 * 813 * @return The new superior DN for a modify DN changelog entry, or 814 * {@code null} if there is no new superior DN, or if this changelog 815 * entry does not represent a modify DN operation. 816 */ 817 @Nullable() 818 public final String getNewSuperior() 819 { 820 return newSuperior; 821 } 822 823 824 825 /** 826 * Retrieves the DN of the entry after the change has been processed. For an 827 * add or modify operation, the new DN will be the same as the target DN. For 828 * a modify DN operation, the new DN will be constructed from the original DN, 829 * the new RDN, and the new superior DN. For a delete operation, it will be 830 * {@code null} because the entry will no longer exist. 831 * 832 * @return The DN of the entry after the change has been processed, or 833 * {@code null} if the entry no longer exists. 834 */ 835 @Nullable() 836 public final String getNewDN() 837 { 838 switch (changeType) 839 { 840 case ADD: 841 case MODIFY: 842 return targetDN; 843 844 case MODIFY_DN: 845 // This will be handled below. 846 break; 847 848 case DELETE: 849 default: 850 return null; 851 } 852 853 try 854 { 855 final RDN parsedNewRDN = new RDN(newRDN); 856 857 if (newSuperior == null) 858 { 859 final DN parsedTargetDN = new DN(targetDN); 860 final DN parentDN = parsedTargetDN.getParent(); 861 if (parentDN == null) 862 { 863 return new DN(parsedNewRDN).toString(); 864 } 865 else 866 { 867 return new DN(parsedNewRDN, parentDN).toString(); 868 } 869 } 870 else 871 { 872 final DN parsedNewSuperior = new DN(newSuperior); 873 return new DN(parsedNewRDN, parsedNewSuperior).toString(); 874 } 875 } 876 catch (final Exception e) 877 { 878 // This should never happen. 879 Debug.debugException(e); 880 return null; 881 } 882 } 883 884 885 886 /** 887 * Retrieves an LDIF change record that is analogous to the operation 888 * represented by this changelog entry. 889 * 890 * @return An LDIF change record that is analogous to the operation 891 * represented by this changelog entry. 892 */ 893 @NotNull() 894 public final LDIFChangeRecord toLDIFChangeRecord() 895 { 896 switch (changeType) 897 { 898 case ADD: 899 return new LDIFAddChangeRecord(targetDN, attributes); 900 901 case DELETE: 902 return new LDIFDeleteChangeRecord(targetDN); 903 904 case MODIFY: 905 return new LDIFModifyChangeRecord(targetDN, modifications); 906 907 case MODIFY_DN: 908 return new LDIFModifyDNChangeRecord(targetDN, newRDN, deleteOldRDN, 909 newSuperior); 910 911 default: 912 // This should never happen. 913 return null; 914 } 915 } 916 917 918 919 /** 920 * Processes the operation represented by this changelog entry using the 921 * provided LDAP connection. 922 * 923 * @param connection The connection (or connection pool) to use to process 924 * the operation. 925 * 926 * @return The result of processing the operation. 927 * 928 * @throws LDAPException If the operation could not be processed 929 * successfully. 930 */ 931 @NotNull() 932 public final LDAPResult processChange(@NotNull final LDAPInterface connection) 933 throws LDAPException 934 { 935 switch (changeType) 936 { 937 case ADD: 938 return connection.add(targetDN, attributes); 939 940 case DELETE: 941 return connection.delete(targetDN); 942 943 case MODIFY: 944 return connection.modify(targetDN, modifications); 945 946 case MODIFY_DN: 947 return connection.modifyDN(targetDN, newRDN, deleteOldRDN, newSuperior); 948 949 default: 950 // This should never happen. 951 return null; 952 } 953 } 954}