001/* 002 * Copyright 2012-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2012-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) 2012-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; 037 038 039 040import java.io.OutputStream; 041import java.util.ArrayList; 042import java.util.LinkedHashMap; 043import java.util.List; 044import java.util.TreeSet; 045import java.util.concurrent.atomic.AtomicInteger; 046import java.util.concurrent.atomic.AtomicReference; 047 048import com.unboundid.asn1.ASN1OctetString; 049import com.unboundid.ldap.sdk.BindRequest; 050import com.unboundid.ldap.sdk.Control; 051import com.unboundid.ldap.sdk.DeleteRequest; 052import com.unboundid.ldap.sdk.DereferencePolicy; 053import com.unboundid.ldap.sdk.DN; 054import com.unboundid.ldap.sdk.ExtendedResult; 055import com.unboundid.ldap.sdk.Filter; 056import com.unboundid.ldap.sdk.InternalSDKHelper; 057import com.unboundid.ldap.sdk.LDAPConnection; 058import com.unboundid.ldap.sdk.LDAPConnectionOptions; 059import com.unboundid.ldap.sdk.LDAPException; 060import com.unboundid.ldap.sdk.LDAPResult; 061import com.unboundid.ldap.sdk.LDAPSearchException; 062import com.unboundid.ldap.sdk.ReadOnlyEntry; 063import com.unboundid.ldap.sdk.ResultCode; 064import com.unboundid.ldap.sdk.RootDSE; 065import com.unboundid.ldap.sdk.SearchRequest; 066import com.unboundid.ldap.sdk.SearchResult; 067import com.unboundid.ldap.sdk.SearchScope; 068import com.unboundid.ldap.sdk.SimpleBindRequest; 069import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler; 070import com.unboundid.ldap.sdk.Version; 071import com.unboundid.ldap.sdk.controls.DraftLDUPSubentriesRequestControl; 072import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl; 073import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedRequest; 074import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedResult; 075import com.unboundid.ldap.sdk.unboundidds.controls. 076 OperationPurposeRequestControl; 077import com.unboundid.ldap.sdk.unboundidds.controls. 078 RealAttributesOnlyRequestControl; 079import com.unboundid.ldap.sdk.unboundidds.controls. 080 ReturnConflictEntriesRequestControl; 081import com.unboundid.ldap.sdk.unboundidds.controls. 082 SoftDeletedEntryAccessRequestControl; 083import com.unboundid.ldap.sdk.unboundidds.controls. 084 SuppressReferentialIntegrityUpdatesRequestControl; 085import com.unboundid.ldap.sdk.unboundidds.extensions. 086 GetSubtreeAccessibilityExtendedRequest; 087import com.unboundid.ldap.sdk.unboundidds.extensions. 088 GetSubtreeAccessibilityExtendedResult; 089import com.unboundid.ldap.sdk.unboundidds.extensions. 090 SetSubtreeAccessibilityExtendedRequest; 091import com.unboundid.ldap.sdk.unboundidds.extensions. 092 SubtreeAccessibilityRestriction; 093import com.unboundid.ldap.sdk.unboundidds.extensions. 094 SubtreeAccessibilityState; 095import com.unboundid.util.Debug; 096import com.unboundid.util.MultiServerLDAPCommandLineTool; 097import com.unboundid.util.NotNull; 098import com.unboundid.util.Nullable; 099import com.unboundid.util.ReverseComparator; 100import com.unboundid.util.StaticUtils; 101import com.unboundid.util.ThreadSafety; 102import com.unboundid.util.ThreadSafetyLevel; 103import com.unboundid.util.args.ArgumentException; 104import com.unboundid.util.args.ArgumentParser; 105import com.unboundid.util.args.BooleanArgument; 106import com.unboundid.util.args.DNArgument; 107import com.unboundid.util.args.FileArgument; 108import com.unboundid.util.args.IntegerArgument; 109import com.unboundid.util.args.StringArgument; 110 111import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*; 112 113 114 115/** 116 * This class provides a utility that may be used to move a single entry or a 117 * small subtree of entries from one server to another. 118 * <BR> 119 * <BLOCKQUOTE> 120 * <B>NOTE:</B> This class, and other classes within the 121 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 122 * supported for use against Ping Identity, UnboundID, and 123 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 124 * for proprietary functionality or for external specifications that are not 125 * considered stable or mature enough to be guaranteed to work in an 126 * interoperable way with other types of LDAP servers. 127 * </BLOCKQUOTE> 128 */ 129@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 130public final class MoveSubtree 131 extends MultiServerLDAPCommandLineTool 132 implements UnsolicitedNotificationHandler, MoveSubtreeListener 133{ 134 /** 135 * The name of the attribute that appears in the root DSE of Ping 136 * Identity, UnboundID, and Nokia/Alcatel-Lucent 8661 Directory Server 137 * instances to provide a unique identifier that will be generated every time 138 * the server starts. 139 */ 140 @NotNull private static final String ATTR_STARTUP_UUID = "startupUUID"; 141 142 143 144 // The argument used to indicate whether to operate in verbose mode. 145 @Nullable private BooleanArgument verbose = null; 146 147 // The argument used to specify the base DNs of the subtrees to move. 148 @Nullable private DNArgument baseDN = null; 149 150 // The argument used to specify a file with base DNs of the subtrees to move. 151 @Nullable private FileArgument baseDNFile = null; 152 153 // The argument used to specify the maximum number of entries to move. 154 @Nullable private IntegerArgument sizeLimit = null; 155 156 // A message that will be displayed if the tool is interrupted. 157 @Nullable private volatile String interruptMessage = null; 158 159 // The argument used to specify the purpose for the move. 160 @Nullable private StringArgument purpose = null; 161 162 163 164 /** 165 * Parse the provided command line arguments and perform the appropriate 166 * processing. 167 * 168 * @param args The command line arguments provided to this program. 169 */ 170 public static void main(@NotNull final String... args) 171 { 172 final ResultCode rc = main(args, System.out, System.err); 173 if (rc != ResultCode.SUCCESS) 174 { 175 System.exit(Math.max(rc.intValue(), 255)); 176 } 177 } 178 179 180 181 /** 182 * Parse the provided command line arguments and perform the appropriate 183 * processing. 184 * 185 * @param args The command line arguments provided to this program. 186 * @param out The output stream to which standard out should be written. 187 * It may be {@code null} if output should be suppressed. 188 * @param err The output stream to which standard error should be written. 189 * It may be {@code null} if error messages should be 190 * suppressed. 191 * 192 * @return A result code indicating whether the processing was successful. 193 */ 194 @NotNull() 195 public static ResultCode main(@NotNull final String[] args, 196 @Nullable final OutputStream out, 197 @Nullable final OutputStream err) 198 { 199 final MoveSubtree moveSubtree = new MoveSubtree(out, err); 200 return moveSubtree.runTool(args); 201 } 202 203 204 205 /** 206 * Creates a new instance of this tool with the provided output and error 207 * streams. 208 * 209 * @param out The output stream to which standard out should be written. It 210 * may be {@code null} if output should be suppressed. 211 * @param err The output stream to which standard error should be written. 212 * It may be {@code null} if error messages should be suppressed. 213 */ 214 public MoveSubtree(@Nullable final OutputStream out, 215 @Nullable final OutputStream err) 216 { 217 super(out, err, new String[] { "source", "target" }, null); 218 } 219 220 221 222 /** 223 * {@inheritDoc} 224 */ 225 @Override() 226 @NotNull() 227 public String getToolName() 228 { 229 return "move-subtree"; 230 } 231 232 233 234 /** 235 * {@inheritDoc} 236 */ 237 @Override() 238 @NotNull() 239 public String getToolDescription() 240 { 241 return INFO_MOVE_SUBTREE_TOOL_DESCRIPTION.get(); 242 } 243 244 245 246 /** 247 * {@inheritDoc} 248 */ 249 @Override() 250 @NotNull() 251 public String getToolVersion() 252 { 253 return Version.NUMERIC_VERSION_STRING; 254 } 255 256 257 258 /** 259 * {@inheritDoc} 260 */ 261 @Override() 262 protected boolean includeAlternateLongIdentifiers() 263 { 264 return true; 265 } 266 267 268 269 /** 270 * {@inheritDoc} 271 */ 272 @Override() 273 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 274 throws ArgumentException 275 { 276 baseDN = new DNArgument('b', "baseDN", false, 0, 277 INFO_MOVE_SUBTREE_ARG_BASE_DN_PLACEHOLDER.get(), 278 INFO_MOVE_SUBTREE_ARG_BASE_DN_DESCRIPTION.get()); 279 baseDN.addLongIdentifier("entryDN", true); 280 parser.addArgument(baseDN); 281 282 baseDNFile = new FileArgument('f', "baseDNFile", false, 1, 283 INFO_MOVE_SUBTREE_ARG_BASE_DN_FILE_PLACEHOLDER.get(), 284 INFO_MOVE_SUBTREE_ARG_BASE_DN_FILE_DESCRIPTION.get(), true, true, 285 true, false); 286 baseDNFile.addLongIdentifier("entryDNFile", true); 287 parser.addArgument(baseDNFile); 288 289 sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, 290 INFO_MOVE_SUBTREE_ARG_SIZE_LIMIT_PLACEHOLDER.get(), 291 INFO_MOVE_SUBTREE_ARG_SIZE_LIMIT_DESCRIPTION.get(), 0, 292 Integer.MAX_VALUE, 0); 293 parser.addArgument(sizeLimit); 294 295 purpose = new StringArgument(null, "purpose", false, 1, 296 INFO_MOVE_SUBTREE_ARG_PURPOSE_PLACEHOLDER.get(), 297 INFO_MOVE_SUBTREE_ARG_PURPOSE_DESCRIPTION.get()); 298 parser.addArgument(purpose); 299 300 verbose = new BooleanArgument('v', "verbose", 1, 301 INFO_MOVE_SUBTREE_ARG_VERBOSE_DESCRIPTION.get()); 302 parser.addArgument(verbose); 303 304 parser.addRequiredArgumentSet(baseDN, baseDNFile); 305 parser.addExclusiveArgumentSet(baseDN, baseDNFile); 306 } 307 308 309 310 /** 311 * {@inheritDoc} 312 */ 313 @Override() 314 @NotNull() 315 public LDAPConnectionOptions getConnectionOptions() 316 { 317 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 318 options.setUnsolicitedNotificationHandler(this); 319 return options; 320 } 321 322 323 324 /** 325 * Indicates whether this tool should provide arguments for redirecting output 326 * to a file. If this method returns {@code true}, then the tool will offer 327 * an "--outputFile" argument that will specify the path to a file to which 328 * all standard output and standard error content will be written, and it will 329 * also offer a "--teeToStandardOut" argument that can only be used if the 330 * "--outputFile" argument is present and will cause all output to be written 331 * to both the specified output file and to standard output. 332 * 333 * @return {@code true} if this tool should provide arguments for redirecting 334 * output to a file, or {@code false} if not. 335 */ 336 @Override() 337 protected boolean supportsOutputFile() 338 { 339 return true; 340 } 341 342 343 344 /** 345 * Indicates whether this tool supports the use of a properties file for 346 * specifying default values for arguments that aren't specified on the 347 * command line. 348 * 349 * @return {@code true} if this tool supports the use of a properties file 350 * for specifying default values for arguments that aren't specified 351 * on the command line, or {@code false} if not. 352 */ 353 @Override() 354 public boolean supportsPropertiesFile() 355 { 356 return true; 357 } 358 359 360 361 /** 362 * {@inheritDoc} 363 */ 364 @Override() 365 protected boolean supportsDebugLogging() 366 { 367 return true; 368 } 369 370 371 372 /** 373 * {@inheritDoc} 374 */ 375 @Override() 376 protected boolean logToolInvocationByDefault() 377 { 378 return true; 379 } 380 381 382 383 /** 384 * {@inheritDoc} 385 */ 386 @Override() 387 @NotNull() 388 public ResultCode doToolProcessing() 389 { 390 final List<String> baseDNs; 391 if (baseDN.isPresent()) 392 { 393 final List<DN> dnList = baseDN.getValues(); 394 baseDNs = new ArrayList<>(dnList.size()); 395 for (final DN dn : dnList) 396 { 397 baseDNs.add(dn.toString()); 398 } 399 } 400 else 401 { 402 try 403 { 404 baseDNs = baseDNFile.getNonBlankFileLines(); 405 } 406 catch (final Exception e) 407 { 408 Debug.debugException(e); 409 err(ERR_MOVE_SUBTREE_ERROR_READING_BASE_DN_FILE.get( 410 baseDNFile.getValue().getAbsolutePath(), 411 StaticUtils.getExceptionMessage(e))); 412 return ResultCode.LOCAL_ERROR; 413 } 414 415 if (baseDNs.isEmpty()) 416 { 417 err(ERR_MOVE_SUBTREE_BASE_DN_FILE_EMPTY.get( 418 baseDNFile.getValue().getAbsolutePath())); 419 return ResultCode.PARAM_ERROR; 420 } 421 } 422 423 424 LDAPConnection sourceConnection = null; 425 LDAPConnection targetConnection = null; 426 427 try 428 { 429 try 430 { 431 sourceConnection = getConnection(0); 432 } 433 catch (final LDAPException le) 434 { 435 Debug.debugException(le); 436 err(ERR_MOVE_SUBTREE_CANNOT_CONNECT_TO_SOURCE.get( 437 StaticUtils.getExceptionMessage(le))); 438 return le.getResultCode(); 439 } 440 441 try 442 { 443 targetConnection = getConnection(1); 444 } 445 catch (final LDAPException le) 446 { 447 Debug.debugException(le); 448 err(ERR_MOVE_SUBTREE_CANNOT_CONNECT_TO_TARGET.get( 449 StaticUtils.getExceptionMessage(le))); 450 return le.getResultCode(); 451 } 452 453 sourceConnection.setConnectionName( 454 INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get()); 455 targetConnection.setConnectionName( 456 INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get()); 457 458 459 // We don't want to accidentally run with the same source and target 460 // servers, so perform a couple of checks to verify that isn't the case. 461 // First, perform a cheap check to rule out using the same address and 462 // port for both source and target servers. 463 if (sourceConnection.getConnectedAddress().equals( 464 targetConnection.getConnectedAddress()) && 465 (sourceConnection.getConnectedPort() == 466 targetConnection.getConnectedPort())) 467 { 468 err(ERR_MOVE_SUBTREE_SAME_SOURCE_AND_TARGET_SERVERS.get()); 469 return ResultCode.PARAM_ERROR; 470 } 471 472 // Next, retrieve the root DSE over each connection. Use it to verify 473 // that both the startupUUID values are different as a check to ensure 474 // that the source and target servers are different (this will be a 475 // best-effort attempt, so if either startupUUID can't be retrieved, then 476 // assume they're different servers). Also check to see whether the 477 // source server supports the suppress referential integrity updates 478 // control. 479 boolean suppressReferentialIntegrityUpdates = false; 480 try 481 { 482 final RootDSE sourceRootDSE = sourceConnection.getRootDSE(); 483 final RootDSE targetRootDSE = targetConnection.getRootDSE(); 484 485 if ((sourceRootDSE != null) && (targetRootDSE != null)) 486 { 487 final String sourceStartupUUID = 488 sourceRootDSE.getAttributeValue(ATTR_STARTUP_UUID); 489 final String targetStartupUUID = 490 targetRootDSE.getAttributeValue(ATTR_STARTUP_UUID); 491 492 if ((sourceStartupUUID != null) && 493 sourceStartupUUID.equals(targetStartupUUID)) 494 { 495 err(ERR_MOVE_SUBTREE_SAME_SOURCE_AND_TARGET_SERVERS.get()); 496 return ResultCode.PARAM_ERROR; 497 } 498 } 499 500 if (sourceRootDSE != null) 501 { 502 suppressReferentialIntegrityUpdates = sourceRootDSE.supportsControl( 503 SuppressReferentialIntegrityUpdatesRequestControl. 504 SUPPRESS_REFINT_REQUEST_OID); 505 } 506 } 507 catch (final Exception e) 508 { 509 Debug.debugException(e); 510 } 511 512 513 boolean first = true; 514 ResultCode resultCode = ResultCode.SUCCESS; 515 for (final String dn : baseDNs) 516 { 517 if (first) 518 { 519 first = false; 520 } 521 else 522 { 523 out(); 524 } 525 526 final OperationPurposeRequestControl operationPurpose; 527 if (purpose.isPresent()) 528 { 529 operationPurpose = new OperationPurposeRequestControl( 530 getToolName(), getToolVersion(), 20, purpose.getValue()); 531 } 532 else 533 { 534 operationPurpose = null; 535 } 536 537 final MoveSubtreeResult result = moveSubtreeWithRestrictedAccessibility( 538 this, sourceConnection, targetConnection, dn, sizeLimit.getValue(), 539 operationPurpose, suppressReferentialIntegrityUpdates, 540 (verbose.isPresent() ? this : null)); 541 if (result.getResultCode() == ResultCode.SUCCESS) 542 { 543 wrapOut(0, 79, 544 INFO_MOVE_SUBTREE_RESULT_SUCCESSFUL.get( 545 result.getEntriesAddedToTarget(), dn)); 546 } 547 else 548 { 549 if (resultCode == ResultCode.SUCCESS) 550 { 551 resultCode = result.getResultCode(); 552 } 553 554 wrapErr(0, 79, ERR_MOVE_SUBTREE_RESULT_UNSUCCESSFUL.get()); 555 556 if (result.getErrorMessage() != null) 557 { 558 wrapErr(0, 79, 559 ERR_MOVE_SUBTREE_ERROR_MESSAGE.get(result.getErrorMessage())); 560 } 561 562 if (result.getAdminActionRequired() != null) 563 { 564 wrapErr(0, 79, 565 ERR_MOVE_SUBTREE_ADMIN_ACTION.get( 566 result.getAdminActionRequired())); 567 } 568 } 569 } 570 571 return resultCode; 572 } 573 finally 574 { 575 if (sourceConnection!= null) 576 { 577 sourceConnection.close(); 578 } 579 580 if (targetConnection!= null) 581 { 582 targetConnection.close(); 583 } 584 } 585 } 586 587 588 589 /** 590 * <BLOCKQUOTE> 591 * <B>NOTE:</B> The use of interactive transactions is strongly discouraged 592 * because it can create conditions which are prone to deadlocks between 593 * operations that may significantly affect performance and will result in 594 * the cancellation of one or both operations. Use one of the 595 * {@code moveSubtreeWithRestrictedAccessibility} methods instead. 596 * </BLOCKQUOTE> 597 * Moves a single leaf entry using a pair of interactive transactions. The 598 * logic used to accomplish this is as follows: 599 * <OL> 600 * <LI>Start an interactive transaction in the source server.</LI> 601 * <LI>Start an interactive transaction in the target server.</LI> 602 * <LI>Read the entry from the source server. The search request will have 603 * a subtree scope with a size limit of one, a filter of 604 * "(objectClass=*)", will request all user and operational attributes, 605 * and will include the following request controls: interactive 606 * transaction specification, ManageDsaIT, LDAP subentries, return 607 * conflict entries, soft-deleted entry access, real attributes only, 608 * and operation purpose.</LI> 609 * <LI>Add the entry to the target server. The add request will include the 610 * following controls: interactive transaction specification, ignore 611 * NO-USER-MODIFICATION, and operation purpose.</LI> 612 * <LI>Delete the entry from the source server. The delete request will 613 * include the following controls: interactive transaction 614 * specification, ManageDsaIT, and operation purpose.</LI> 615 * <LI>Commit the interactive transaction in the target server.</LI> 616 * <LI>Commit the interactive transaction in the source server.</LI> 617 * </OL> 618 * Conditions which could result in an incomplete move include: 619 * <UL> 620 * <LI>The commit in the target server succeeds but the commit in the 621 * source server fails. In this case, the entry may end up in both 622 * servers, requiring manual cleanup. If this occurs, then the result 623 * returned from this method will indicate this condition.</LI> 624 * <LI>The account used to read entries from the source server does not have 625 * permission to see all attributes in all entries. In this case, the 626 * target server will include only a partial representation of the entry 627 * in the source server. To avoid this problem, ensure that the account 628 * used to read from the source server has sufficient access rights to 629 * see all attributes in the entry to move.</LI> 630 * <LI>The source server participates in replication and a change occurs to 631 * the entry in a different server in the replicated environment while 632 * the move is in progress. In this case, those changes may not be 633 * reflected in the target server. To avoid this problem, it is 634 * strongly recommended that all write access in the replication 635 * environment containing the source server be directed to the source 636 * server during the time that the move is in progress (e.g., using a 637 * failover load-balancing algorithm in the Directory Proxy 638 * Server).</LI> 639 * </UL> 640 * 641 * @param sourceConnection A connection established to the source server. 642 * It should be authenticated as a user with 643 * permission to perform all of the operations 644 * against the source server as referenced above. 645 * @param targetConnection A connection established to the target server. 646 * It should be authenticated as a user with 647 * permission to perform all of the operations 648 * against the target server as referenced above. 649 * @param entryDN The base DN for the subtree to move. 650 * @param opPurposeControl An optional operation purpose request control 651 * that may be included in all requests sent to the 652 * source and target servers. 653 * @param listener An optional listener that may be invoked during 654 * the course of moving entries from the source 655 * server to the target server. 656 * 657 * @return An object with information about the result of the attempted 658 * subtree move. 659 * 660 * @deprecated The use of interactive transactions is strongly discouraged 661 * because it can create conditions which are prone to deadlocks 662 * between operations that may significantly affect performance 663 * and will result in the cancellation of one or both operations. 664 */ 665 @Deprecated() 666 @NotNull() 667 public static MoveSubtreeResult moveEntryWithInteractiveTransaction( 668 @NotNull final LDAPConnection sourceConnection, 669 @NotNull final LDAPConnection targetConnection, 670 @NotNull final String entryDN, 671 @Nullable final OperationPurposeRequestControl opPurposeControl, 672 @Nullable final MoveSubtreeListener listener) 673 { 674 return moveEntryWithInteractiveTransaction(sourceConnection, 675 targetConnection, entryDN, opPurposeControl, false, listener); 676 } 677 678 679 680 /** 681 * <BLOCKQUOTE> 682 * <B>NOTE:</B> The use of interactive transactions is strongly discouraged 683 * because it can create conditions which are prone to deadlocks between 684 * operations that may significantly affect performance and will result in 685 * the cancellation of one or both operations. Use one of the 686 * {@code moveSubtreeWithRestrictedAccessibility} methods instead. 687 * </BLOCKQUOTE> 688 * Moves a single leaf entry using a pair of interactive transactions. The 689 * logic used to accomplish this is as follows: 690 * <OL> 691 * <LI>Start an interactive transaction in the source server.</LI> 692 * <LI>Start an interactive transaction in the target server.</LI> 693 * <LI>Read the entry from the source server. The search request will have 694 * a subtree scope with a size limit of one, a filter of 695 * "(objectClass=*)", will request all user and operational attributes, 696 * and will include the following request controls: interactive 697 * transaction specification, ManageDsaIT, LDAP subentries, return 698 * conflict entries, soft-deleted entry access, real attributes only, 699 * and operation purpose.</LI> 700 * <LI>Add the entry to the target server. The add request will include the 701 * following controls: interactive transaction specification, ignore 702 * NO-USER-MODIFICATION, and operation purpose.</LI> 703 * <LI>Delete the entry from the source server. The delete request will 704 * include the following controls: interactive transaction 705 * specification, ManageDsaIT, and operation purpose.</LI> 706 * <LI>Commit the interactive transaction in the target server.</LI> 707 * <LI>Commit the interactive transaction in the source server.</LI> 708 * </OL> 709 * Conditions which could result in an incomplete move include: 710 * <UL> 711 * <LI>The commit in the target server succeeds but the commit in the 712 * source server fails. In this case, the entry may end up in both 713 * servers, requiring manual cleanup. If this occurs, then the result 714 * returned from this method will indicate this condition.</LI> 715 * <LI>The account used to read entries from the source server does not have 716 * permission to see all attributes in all entries. In this case, the 717 * target server will include only a partial representation of the entry 718 * in the source server. To avoid this problem, ensure that the account 719 * used to read from the source server has sufficient access rights to 720 * see all attributes in the entry to move.</LI> 721 * <LI>The source server participates in replication and a change occurs to 722 * the entry in a different server in the replicated environment while 723 * the move is in progress. In this case, those changes may not be 724 * reflected in the target server. To avoid this problem, it is 725 * strongly recommended that all write access in the replication 726 * environment containing the source server be directed to the source 727 * server during the time that the move is in progress (e.g., using a 728 * failover load-balancing algorithm in the Directory Proxy 729 * Server).</LI> 730 * </UL> 731 * 732 * @param sourceConnection A connection established to the source server. 733 * It should be authenticated as a user with 734 * permission to perform all of the operations 735 * against the source server as referenced above. 736 * @param targetConnection A connection established to the target server. 737 * It should be authenticated as a user with 738 * permission to perform all of the operations 739 * against the target server as referenced above. 740 * @param entryDN The base DN for the subtree to move. 741 * @param opPurposeControl An optional operation purpose request control 742 * that may be included in all requests sent to the 743 * source and target servers. 744 * @param suppressRefInt Indicates whether to include a request control 745 * causing referential integrity updates to be 746 * suppressed on the source server. 747 * @param listener An optional listener that may be invoked during 748 * the course of moving entries from the source 749 * server to the target server. 750 * 751 * @return An object with information about the result of the attempted 752 * subtree move. 753 * 754 * @deprecated The use of interactive transactions is strongly discouraged 755 * because it can create conditions which are prone to deadlocks 756 * between operations that may significantly affect performance 757 * and will result in the cancellation of one or both operations. 758 */ 759 @Deprecated() 760 @SuppressWarnings("deprecation") 761 @NotNull() 762 public static MoveSubtreeResult moveEntryWithInteractiveTransaction( 763 @NotNull final LDAPConnection sourceConnection, 764 @NotNull final LDAPConnection targetConnection, 765 @NotNull final String entryDN, 766 @Nullable final OperationPurposeRequestControl opPurposeControl, 767 final boolean suppressRefInt, 768 @Nullable final MoveSubtreeListener listener) 769 { 770 final StringBuilder errorMsg = new StringBuilder(); 771 final StringBuilder adminMsg = new StringBuilder(); 772 773 final ReverseComparator<DN> reverseComparator = new ReverseComparator<>(); 774 final TreeSet<DN> sourceEntryDNs = new TreeSet<>(reverseComparator); 775 776 final AtomicInteger entriesReadFromSource = new AtomicInteger(0); 777 final AtomicInteger entriesAddedToTarget = new AtomicInteger(0); 778 final AtomicInteger entriesDeletedFromSource = new AtomicInteger(0); 779 final AtomicReference<ResultCode> resultCode = new AtomicReference<>(); 780 781 ASN1OctetString sourceTxnID = null; 782 ASN1OctetString targetTxnID = null; 783 boolean sourceServerAltered = false; 784 boolean targetServerAltered = false; 785 786processingBlock: 787 try 788 { 789 // Start an interactive transaction in the source server. 790 final com.unboundid.ldap.sdk.unboundidds.controls. 791 InteractiveTransactionSpecificationRequestControl sourceTxnControl; 792 try 793 { 794 final com.unboundid.ldap.sdk.unboundidds.extensions. 795 StartInteractiveTransactionExtendedRequest startTxnRequest; 796 if (opPurposeControl == null) 797 { 798 startTxnRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 799 StartInteractiveTransactionExtendedRequest(entryDN); 800 } 801 else 802 { 803 startTxnRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 804 StartInteractiveTransactionExtendedRequest(entryDN, 805 new Control[]{opPurposeControl}); 806 } 807 808 final com.unboundid.ldap.sdk.unboundidds.extensions. 809 StartInteractiveTransactionExtendedResult startTxnResult = 810 (com.unboundid.ldap.sdk.unboundidds.extensions. 811 StartInteractiveTransactionExtendedResult) 812 sourceConnection.processExtendedOperation(startTxnRequest); 813 if (startTxnResult.getResultCode() == ResultCode.SUCCESS) 814 { 815 sourceTxnID = startTxnResult.getTransactionID(); 816 sourceTxnControl = new com.unboundid.ldap.sdk.unboundidds.controls. 817 InteractiveTransactionSpecificationRequestControl(sourceTxnID, 818 true, true); 819 } 820 else 821 { 822 resultCode.compareAndSet(null, startTxnResult.getResultCode()); 823 append( 824 ERR_MOVE_ENTRY_CANNOT_START_SOURCE_TXN.get( 825 startTxnResult.getDiagnosticMessage()), 826 errorMsg); 827 break processingBlock; 828 } 829 } 830 catch (final LDAPException le) 831 { 832 Debug.debugException(le); 833 resultCode.compareAndSet(null, le.getResultCode()); 834 append( 835 ERR_MOVE_ENTRY_CANNOT_START_SOURCE_TXN.get( 836 StaticUtils.getExceptionMessage(le)), 837 errorMsg); 838 break processingBlock; 839 } 840 841 842 // Start an interactive transaction in the target server. 843 final com.unboundid.ldap.sdk.unboundidds.controls. 844 InteractiveTransactionSpecificationRequestControl targetTxnControl; 845 try 846 { 847 final com.unboundid.ldap.sdk.unboundidds.extensions. 848 StartInteractiveTransactionExtendedRequest startTxnRequest; 849 if (opPurposeControl == null) 850 { 851 startTxnRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 852 StartInteractiveTransactionExtendedRequest(entryDN); 853 } 854 else 855 { 856 startTxnRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 857 StartInteractiveTransactionExtendedRequest(entryDN, 858 new Control[]{opPurposeControl}); 859 } 860 861 final com.unboundid.ldap.sdk.unboundidds.extensions. 862 StartInteractiveTransactionExtendedResult startTxnResult = 863 (com.unboundid.ldap.sdk.unboundidds.extensions. 864 StartInteractiveTransactionExtendedResult) 865 targetConnection.processExtendedOperation(startTxnRequest); 866 if (startTxnResult.getResultCode() == ResultCode.SUCCESS) 867 { 868 targetTxnID = startTxnResult.getTransactionID(); 869 targetTxnControl = new com.unboundid.ldap.sdk.unboundidds.controls. 870 InteractiveTransactionSpecificationRequestControl(targetTxnID, 871 true, true); 872 } 873 else 874 { 875 resultCode.compareAndSet(null, startTxnResult.getResultCode()); 876 append( 877 ERR_MOVE_ENTRY_CANNOT_START_TARGET_TXN.get( 878 startTxnResult.getDiagnosticMessage()), 879 errorMsg); 880 break processingBlock; 881 } 882 } 883 catch (final LDAPException le) 884 { 885 Debug.debugException(le); 886 resultCode.compareAndSet(null, le.getResultCode()); 887 append( 888 ERR_MOVE_ENTRY_CANNOT_START_TARGET_TXN.get( 889 StaticUtils.getExceptionMessage(le)), 890 errorMsg); 891 break processingBlock; 892 } 893 894 895 // Perform a search to find all entries in the target subtree, and include 896 // a search listener that will add each entry to the target server as it 897 // is returned from the source server. 898 final Control[] searchControls; 899 if (opPurposeControl == null) 900 { 901 searchControls = new Control[] 902 { 903 sourceTxnControl, 904 new DraftLDUPSubentriesRequestControl(true), 905 new ManageDsaITRequestControl(true), 906 new ReturnConflictEntriesRequestControl(true), 907 new SoftDeletedEntryAccessRequestControl(true, true, false), 908 new RealAttributesOnlyRequestControl(true) 909 }; 910 } 911 else 912 { 913 searchControls = new Control[] 914 { 915 sourceTxnControl, 916 new DraftLDUPSubentriesRequestControl(true), 917 new ManageDsaITRequestControl(true), 918 new ReturnConflictEntriesRequestControl(true), 919 new SoftDeletedEntryAccessRequestControl(true, true, false), 920 new RealAttributesOnlyRequestControl(true), 921 opPurposeControl 922 }; 923 } 924 925 final MoveSubtreeTxnSearchListener searchListener = 926 new MoveSubtreeTxnSearchListener(targetConnection, resultCode, 927 errorMsg, entriesReadFromSource, entriesAddedToTarget, 928 sourceEntryDNs, targetTxnControl, opPurposeControl, listener); 929 final SearchRequest searchRequest = new SearchRequest( 930 searchListener, searchControls, entryDN, SearchScope.SUB, 931 DereferencePolicy.NEVER, 1, 0, false, 932 Filter.createPresenceFilter("objectClass"), "*", "+"); 933 934 SearchResult searchResult; 935 try 936 { 937 searchResult = sourceConnection.search(searchRequest); 938 } 939 catch (final LDAPSearchException lse) 940 { 941 Debug.debugException(lse); 942 searchResult = lse.getSearchResult(); 943 } 944 945 if (searchResult.getResultCode() == ResultCode.SUCCESS) 946 { 947 try 948 { 949 final com.unboundid.ldap.sdk.unboundidds.controls. 950 InteractiveTransactionSpecificationResponseControl txnResult = 951 com.unboundid.ldap.sdk.unboundidds.controls. 952 InteractiveTransactionSpecificationResponseControl.get( 953 searchResult); 954 if ((txnResult == null) || (! txnResult.transactionValid())) 955 { 956 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 957 append(ERR_MOVE_ENTRY_SEARCH_TXN_NO_LONGER_VALID.get(), 958 errorMsg); 959 break processingBlock; 960 } 961 } 962 catch (final LDAPException le) 963 { 964 Debug.debugException(le); 965 resultCode.compareAndSet(null, le.getResultCode()); 966 append( 967 ERR_MOVE_ENTRY_CANNOT_DECODE_SEARCH_TXN_CONTROL.get( 968 StaticUtils.getExceptionMessage(le)), 969 errorMsg); 970 break processingBlock; 971 } 972 } 973 else 974 { 975 resultCode.compareAndSet(null, searchResult.getResultCode()); 976 append( 977 ERR_MOVE_SUBTREE_SEARCH_FAILED.get(entryDN, 978 searchResult.getDiagnosticMessage()), 979 errorMsg); 980 981 try 982 { 983 final com.unboundid.ldap.sdk.unboundidds.controls. 984 InteractiveTransactionSpecificationResponseControl txnResult = 985 com.unboundid.ldap.sdk.unboundidds.controls. 986 InteractiveTransactionSpecificationResponseControl.get( 987 searchResult); 988 if ((txnResult != null) && (! txnResult.transactionValid())) 989 { 990 sourceTxnID = null; 991 } 992 } 993 catch (final LDAPException le) 994 { 995 Debug.debugException(le); 996 } 997 998 if (! searchListener.targetTransactionValid()) 999 { 1000 targetTxnID = null; 1001 } 1002 1003 break processingBlock; 1004 } 1005 1006 // If an error occurred during add processing, then fail. 1007 if (resultCode.get() == null) 1008 { 1009 targetServerAltered = true; 1010 } 1011 else 1012 { 1013 break processingBlock; 1014 } 1015 1016 1017 // Delete each of the entries in the source server. The map should 1018 // already be sorted in reverse order (as a result of the comparator used 1019 // when creating it), so it will guarantee children are deleted before 1020 // their parents. 1021 final ArrayList<Control> deleteControlList = new ArrayList<>(4); 1022 deleteControlList.add(sourceTxnControl); 1023 deleteControlList.add(new ManageDsaITRequestControl(true)); 1024 if (opPurposeControl != null) 1025 { 1026 deleteControlList.add(opPurposeControl); 1027 } 1028 if (suppressRefInt) 1029 { 1030 deleteControlList.add( 1031 new SuppressReferentialIntegrityUpdatesRequestControl(false)); 1032 } 1033 1034 final Control[] deleteControls = new Control[deleteControlList.size()]; 1035 deleteControlList.toArray(deleteControls); 1036 for (final DN dn : sourceEntryDNs) 1037 { 1038 if (listener != null) 1039 { 1040 try 1041 { 1042 listener.doPreDeleteProcessing(dn); 1043 } 1044 catch (final Exception e) 1045 { 1046 Debug.debugException(e); 1047 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1048 append( 1049 ERR_MOVE_SUBTREE_PRE_DELETE_FAILURE.get(dn.toString(), 1050 StaticUtils.getExceptionMessage(e)), 1051 errorMsg); 1052 break processingBlock; 1053 } 1054 } 1055 1056 LDAPResult deleteResult; 1057 try 1058 { 1059 deleteResult = sourceConnection.delete( 1060 new DeleteRequest(dn, deleteControls)); 1061 } 1062 catch (final LDAPException le) 1063 { 1064 Debug.debugException(le); 1065 deleteResult = le.toLDAPResult(); 1066 } 1067 1068 if (deleteResult.getResultCode() == ResultCode.SUCCESS) 1069 { 1070 sourceServerAltered = true; 1071 entriesDeletedFromSource.incrementAndGet(); 1072 1073 try 1074 { 1075 final com.unboundid.ldap.sdk.unboundidds.controls. 1076 InteractiveTransactionSpecificationResponseControl txnResult = 1077 com.unboundid.ldap.sdk.unboundidds.controls. 1078 InteractiveTransactionSpecificationResponseControl.get( 1079 deleteResult); 1080 if ((txnResult == null) || (! txnResult.transactionValid())) 1081 { 1082 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1083 append( 1084 ERR_MOVE_ENTRY_DELETE_TXN_NO_LONGER_VALID.get( 1085 dn.toString()), 1086 errorMsg); 1087 break processingBlock; 1088 } 1089 } 1090 catch (final LDAPException le) 1091 { 1092 Debug.debugException(le); 1093 resultCode.compareAndSet(null, le.getResultCode()); 1094 append( 1095 ERR_MOVE_ENTRY_CANNOT_DECODE_DELETE_TXN_CONTROL.get( 1096 dn.toString(), StaticUtils.getExceptionMessage(le)), 1097 errorMsg); 1098 break processingBlock; 1099 } 1100 } 1101 else 1102 { 1103 resultCode.compareAndSet(null, deleteResult.getResultCode()); 1104 append( 1105 ERR_MOVE_SUBTREE_DELETE_FAILURE.get( 1106 dn.toString(), deleteResult.getDiagnosticMessage()), 1107 errorMsg); 1108 1109 try 1110 { 1111 final com.unboundid.ldap.sdk.unboundidds.controls. 1112 InteractiveTransactionSpecificationResponseControl txnResult = 1113 com.unboundid.ldap.sdk.unboundidds.controls. 1114 InteractiveTransactionSpecificationResponseControl.get( 1115 deleteResult); 1116 if ((txnResult != null) && (! txnResult.transactionValid())) 1117 { 1118 sourceTxnID = null; 1119 } 1120 } 1121 catch (final LDAPException le) 1122 { 1123 Debug.debugException(le); 1124 } 1125 1126 break processingBlock; 1127 } 1128 1129 if (listener != null) 1130 { 1131 try 1132 { 1133 listener.doPostDeleteProcessing(dn); 1134 } 1135 catch (final Exception e) 1136 { 1137 Debug.debugException(e); 1138 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1139 append( 1140 ERR_MOVE_SUBTREE_POST_DELETE_FAILURE.get(dn.toString(), 1141 StaticUtils.getExceptionMessage(e)), 1142 errorMsg); 1143 break processingBlock; 1144 } 1145 } 1146 } 1147 1148 1149 // Commit the transaction in the target server. 1150 try 1151 { 1152 final com.unboundid.ldap.sdk.unboundidds.extensions. 1153 EndInteractiveTransactionExtendedRequest commitRequest; 1154 if (opPurposeControl == null) 1155 { 1156 commitRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1157 EndInteractiveTransactionExtendedRequest(targetTxnID, true); 1158 } 1159 else 1160 { 1161 commitRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1162 EndInteractiveTransactionExtendedRequest(targetTxnID, true, 1163 new Control[] { opPurposeControl }); 1164 } 1165 1166 final ExtendedResult commitResult = 1167 targetConnection.processExtendedOperation(commitRequest); 1168 if (commitResult.getResultCode() == ResultCode.SUCCESS) 1169 { 1170 targetTxnID = null; 1171 } 1172 else 1173 { 1174 resultCode.compareAndSet(null, commitResult.getResultCode()); 1175 append( 1176 ERR_MOVE_ENTRY_CANNOT_COMMIT_TARGET_TXN.get( 1177 commitResult.getDiagnosticMessage()), 1178 errorMsg); 1179 break processingBlock; 1180 } 1181 } 1182 catch (final LDAPException le) 1183 { 1184 Debug.debugException(le); 1185 resultCode.compareAndSet(null, le.getResultCode()); 1186 append( 1187 ERR_MOVE_ENTRY_CANNOT_COMMIT_TARGET_TXN.get( 1188 StaticUtils.getExceptionMessage(le)), 1189 errorMsg); 1190 break processingBlock; 1191 } 1192 1193 1194 // Commit the transaction in the source server. 1195 try 1196 { 1197 final com.unboundid.ldap.sdk.unboundidds.extensions. 1198 EndInteractiveTransactionExtendedRequest commitRequest; 1199 if (opPurposeControl == null) 1200 { 1201 commitRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1202 EndInteractiveTransactionExtendedRequest(sourceTxnID, true); 1203 } 1204 else 1205 { 1206 commitRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1207 EndInteractiveTransactionExtendedRequest(sourceTxnID, true, 1208 new Control[] { opPurposeControl }); 1209 } 1210 1211 final ExtendedResult commitResult = 1212 sourceConnection.processExtendedOperation(commitRequest); 1213 if (commitResult.getResultCode() == ResultCode.SUCCESS) 1214 { 1215 sourceTxnID = null; 1216 } 1217 else 1218 { 1219 resultCode.compareAndSet(null, commitResult.getResultCode()); 1220 append( 1221 ERR_MOVE_ENTRY_CANNOT_COMMIT_SOURCE_TXN.get( 1222 commitResult.getDiagnosticMessage()), 1223 errorMsg); 1224 break processingBlock; 1225 } 1226 } 1227 catch (final LDAPException le) 1228 { 1229 Debug.debugException(le); 1230 resultCode.compareAndSet(null, le.getResultCode()); 1231 append( 1232 ERR_MOVE_ENTRY_CANNOT_COMMIT_SOURCE_TXN.get( 1233 StaticUtils.getExceptionMessage(le)), 1234 errorMsg); 1235 append(ERR_MOVE_ENTRY_EXISTS_IN_BOTH_SERVERS.get(entryDN), 1236 adminMsg); 1237 break processingBlock; 1238 } 1239 } 1240 finally 1241 { 1242 // If the transaction is still active in the target server, then abort it. 1243 if (targetTxnID != null) 1244 { 1245 try 1246 { 1247 final com.unboundid.ldap.sdk.unboundidds.extensions. 1248 EndInteractiveTransactionExtendedRequest abortRequest; 1249 if (opPurposeControl == null) 1250 { 1251 abortRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1252 EndInteractiveTransactionExtendedRequest(targetTxnID, false); 1253 } 1254 else 1255 { 1256 abortRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1257 EndInteractiveTransactionExtendedRequest(targetTxnID, false, 1258 new Control[] { opPurposeControl }); 1259 } 1260 1261 final ExtendedResult abortResult = 1262 targetConnection.processExtendedOperation(abortRequest); 1263 if (abortResult.getResultCode() == 1264 ResultCode.INTERACTIVE_TRANSACTION_ABORTED) 1265 { 1266 targetServerAltered = false; 1267 entriesAddedToTarget.set(0); 1268 append(INFO_MOVE_ENTRY_TARGET_ABORT_SUCCEEDED.get(), 1269 errorMsg); 1270 } 1271 else 1272 { 1273 append( 1274 ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE.get( 1275 abortResult.getDiagnosticMessage()), 1276 errorMsg); 1277 append( 1278 ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE_ADMIN_ACTION.get( 1279 entryDN), 1280 adminMsg); 1281 } 1282 } 1283 catch (final Exception e) 1284 { 1285 Debug.debugException(e); 1286 append( 1287 ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE.get( 1288 StaticUtils.getExceptionMessage(e)), 1289 errorMsg); 1290 append( 1291 ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE_ADMIN_ACTION.get( 1292 entryDN), 1293 adminMsg); 1294 } 1295 } 1296 1297 1298 // If the transaction is still active in the source server, then abort it. 1299 if (sourceTxnID != null) 1300 { 1301 try 1302 { 1303 final com.unboundid.ldap.sdk.unboundidds.extensions. 1304 EndInteractiveTransactionExtendedRequest abortRequest; 1305 if (opPurposeControl == null) 1306 { 1307 abortRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1308 EndInteractiveTransactionExtendedRequest(sourceTxnID, false); 1309 } 1310 else 1311 { 1312 abortRequest = new com.unboundid.ldap.sdk.unboundidds.extensions. 1313 EndInteractiveTransactionExtendedRequest(sourceTxnID, false, 1314 new Control[] { opPurposeControl }); 1315 } 1316 1317 final ExtendedResult abortResult = 1318 sourceConnection.processExtendedOperation(abortRequest); 1319 if (abortResult.getResultCode() == 1320 ResultCode.INTERACTIVE_TRANSACTION_ABORTED) 1321 { 1322 sourceServerAltered = false; 1323 entriesDeletedFromSource.set(0); 1324 append(INFO_MOVE_ENTRY_SOURCE_ABORT_SUCCEEDED.get(), 1325 errorMsg); 1326 } 1327 else 1328 { 1329 append( 1330 ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE.get( 1331 abortResult.getDiagnosticMessage()), 1332 errorMsg); 1333 append( 1334 ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE_ADMIN_ACTION.get( 1335 entryDN), 1336 adminMsg); 1337 } 1338 } 1339 catch (final Exception e) 1340 { 1341 Debug.debugException(e); 1342 append( 1343 ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE.get( 1344 StaticUtils.getExceptionMessage(e)), 1345 errorMsg); 1346 append( 1347 ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE_ADMIN_ACTION.get( 1348 entryDN), 1349 adminMsg); 1350 } 1351 } 1352 } 1353 1354 1355 // Construct the result to return to the client. 1356 resultCode.compareAndSet(null, ResultCode.SUCCESS); 1357 1358 final String errorMessage; 1359 if (errorMsg.length() > 0) 1360 { 1361 errorMessage = errorMsg.toString(); 1362 } 1363 else 1364 { 1365 errorMessage = null; 1366 } 1367 1368 final String adminActionRequired; 1369 if (adminMsg.length() > 0) 1370 { 1371 adminActionRequired = adminMsg.toString(); 1372 } 1373 else 1374 { 1375 adminActionRequired = null; 1376 } 1377 1378 return new MoveSubtreeResult(resultCode.get(), errorMessage, 1379 adminActionRequired, sourceServerAltered, targetServerAltered, 1380 entriesReadFromSource.get(), entriesAddedToTarget.get(), 1381 entriesDeletedFromSource.get()); 1382 } 1383 1384 1385 1386 /** 1387 * Moves a subtree of entries using a process in which access to the subtree 1388 * will be restricted while the move is in progress. While entries are being 1389 * read from the source server and added to the target server, the subtree 1390 * will be read-only in the source server and hidden in the target server. 1391 * While entries are being removed from the source server, the subtree will be 1392 * hidden in the source server while fully accessible in the target. After 1393 * all entries have been removed from the source server, the accessibility 1394 * restriction will be removed from that server as well. 1395 * <BR><BR> 1396 * The logic used to accomplish this is as follows: 1397 * <OL> 1398 * <LI>Make the subtree hidden in the target server.</LI> 1399 * <LI>Make the subtree read-only in the source server.</LI> 1400 * <LI>Perform a search in the source server to retrieve all entries in the 1401 * specified subtree. The search request will have a subtree scope with 1402 * a filter of "(objectClass=*)", will include the specified size limit, 1403 * will request all user and operational attributes, and will include 1404 * the following request controls: ManageDsaIT, LDAP subentries, 1405 * return conflict entries, soft-deleted entry access, real attributes 1406 * only, and operation purpose.</LI> 1407 * <LI>For each entry returned by the search, add that entry to the target 1408 * server. This method assumes that the source server will return 1409 * results in a manner that guarantees that no child entry is returned 1410 * before its parent. Each add request will include the following 1411 * controls: ignore NO-USER-MODIFICATION, and operation purpose.</LI> 1412 * <LI>Make the subtree read-only in the target server.</LI> 1413 * <LI>Make the subtree hidden in the source server.</LI> 1414 * <LI>Make the subtree accessible in the target server.</LI> 1415 * <LI>Delete each entry from the source server, with all subordinate entries 1416 * before their parents. Each delete request will include the following 1417 * controls: ManageDsaIT, and operation purpose.</LI> 1418 * <LI>Make the subtree accessible in the source server.</LI> 1419 * </OL> 1420 * Conditions which could result in an incomplete move include: 1421 * <UL> 1422 * <LI>A failure is encountered while altering the accessibility of the 1423 * subtree in either the source or target server.</LI> 1424 * <LI>A failure is encountered while attempting to process an add in the 1425 * target server and a subsequent failure is encountered when attempting 1426 * to delete previously-added entries.</LI> 1427 * <LI>A failure is encountered while attempting to delete one or more 1428 * entries from the source server.</LI> 1429 * </UL> 1430 * 1431 * @param sourceConnection A connection established to the source server. 1432 * It should be authenticated as a user with 1433 * permission to perform all of the operations 1434 * against the source server as referenced above. 1435 * @param targetConnection A connection established to the target server. 1436 * It should be authenticated as a user with 1437 * permission to perform all of the operations 1438 * against the target server as referenced above. 1439 * @param baseDN The base DN for the subtree to move. 1440 * @param sizeLimit The maximum number of entries to be moved. It 1441 * may be less than or equal to zero to indicate 1442 * that no client-side limit should be enforced 1443 * (although the server may still enforce its own 1444 * limit). 1445 * @param opPurposeControl An optional operation purpose request control 1446 * that may be included in all requests sent to the 1447 * source and target servers. 1448 * @param listener An optional listener that may be invoked during 1449 * the course of moving entries from the source 1450 * server to the target server. 1451 * 1452 * @return An object with information about the result of the attempted 1453 * subtree move. 1454 */ 1455 @NotNull() 1456 public static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility( 1457 @NotNull final LDAPConnection sourceConnection, 1458 @NotNull final LDAPConnection targetConnection, 1459 @NotNull final String baseDN, final int sizeLimit, 1460 @Nullable final OperationPurposeRequestControl opPurposeControl, 1461 @Nullable final MoveSubtreeListener listener) 1462 { 1463 return moveSubtreeWithRestrictedAccessibility(sourceConnection, 1464 targetConnection, baseDN, sizeLimit, opPurposeControl, false, 1465 listener); 1466 } 1467 1468 1469 1470 /** 1471 * Moves a subtree of entries using a process in which access to the subtree 1472 * will be restricted while the move is in progress. While entries are being 1473 * read from the source server and added to the target server, the subtree 1474 * will be read-only in the source server and hidden in the target server. 1475 * While entries are being removed from the source server, the subtree will be 1476 * hidden in the source server while fully accessible in the target. After 1477 * all entries have been removed from the source server, the accessibility 1478 * restriction will be removed from that server as well. 1479 * <BR><BR> 1480 * The logic used to accomplish this is as follows: 1481 * <OL> 1482 * <LI>Make the subtree hidden in the target server.</LI> 1483 * <LI>Make the subtree read-only in the source server.</LI> 1484 * <LI>Perform a search in the source server to retrieve all entries in the 1485 * specified subtree. The search request will have a subtree scope with 1486 * a filter of "(objectClass=*)", will include the specified size limit, 1487 * will request all user and operational attributes, and will include 1488 * the following request controls: ManageDsaIT, LDAP subentries, 1489 * return conflict entries, soft-deleted entry access, real attributes 1490 * only, and operation purpose.</LI> 1491 * <LI>For each entry returned by the search, add that entry to the target 1492 * server. This method assumes that the source server will return 1493 * results in a manner that guarantees that no child entry is returned 1494 * before its parent. Each add request will include the following 1495 * controls: ignore NO-USER-MODIFICATION, and operation purpose.</LI> 1496 * <LI>Make the subtree read-only in the target server.</LI> 1497 * <LI>Make the subtree hidden in the source server.</LI> 1498 * <LI>Make the subtree accessible in the target server.</LI> 1499 * <LI>Delete each entry from the source server, with all subordinate entries 1500 * before their parents. Each delete request will include the following 1501 * controls: ManageDsaIT, and operation purpose.</LI> 1502 * <LI>Make the subtree accessible in the source server.</LI> 1503 * </OL> 1504 * Conditions which could result in an incomplete move include: 1505 * <UL> 1506 * <LI>A failure is encountered while altering the accessibility of the 1507 * subtree in either the source or target server.</LI> 1508 * <LI>A failure is encountered while attempting to process an add in the 1509 * target server and a subsequent failure is encountered when attempting 1510 * to delete previously-added entries.</LI> 1511 * <LI>A failure is encountered while attempting to delete one or more 1512 * entries from the source server.</LI> 1513 * </UL> 1514 * 1515 * @param sourceConnection A connection established to the source server. 1516 * It should be authenticated as a user with 1517 * permission to perform all of the operations 1518 * against the source server as referenced above. 1519 * @param targetConnection A connection established to the target server. 1520 * It should be authenticated as a user with 1521 * permission to perform all of the operations 1522 * against the target server as referenced above. 1523 * @param baseDN The base DN for the subtree to move. 1524 * @param sizeLimit The maximum number of entries to be moved. It 1525 * may be less than or equal to zero to indicate 1526 * that no client-side limit should be enforced 1527 * (although the server may still enforce its own 1528 * limit). 1529 * @param opPurposeControl An optional operation purpose request control 1530 * that may be included in all requests sent to the 1531 * source and target servers. 1532 * @param suppressRefInt Indicates whether to include a request control 1533 * causing referential integrity updates to be 1534 * suppressed on the source server. 1535 * @param listener An optional listener that may be invoked during 1536 * the course of moving entries from the source 1537 * server to the target server. 1538 * 1539 * @return An object with information about the result of the attempted 1540 * subtree move. 1541 */ 1542 @NotNull() 1543 public static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility( 1544 @NotNull final LDAPConnection sourceConnection, 1545 @NotNull final LDAPConnection targetConnection, 1546 @NotNull final String baseDN, final int sizeLimit, 1547 @Nullable final OperationPurposeRequestControl opPurposeControl, 1548 final boolean suppressRefInt, 1549 @Nullable final MoveSubtreeListener listener) 1550 { 1551 return moveSubtreeWithRestrictedAccessibility(null, sourceConnection, 1552 targetConnection, baseDN, sizeLimit, opPurposeControl, suppressRefInt, 1553 listener); 1554 } 1555 1556 1557 1558 /** 1559 * Performs the real {@code moveSubtreeWithRestrictedAccessibility} 1560 * processing. If a tool is available, this method will update state 1561 * information in that tool so that it can be referenced by a shutdown hook 1562 * in the event that processing is interrupted. 1563 * 1564 * @param tool A reference to a tool instance to be updated with 1565 * state information. 1566 * @param sourceConnection A connection established to the source server. 1567 * It should be authenticated as a user with 1568 * permission to perform all of the operations 1569 * against the source server as referenced above. 1570 * @param targetConnection A connection established to the target server. 1571 * It should be authenticated as a user with 1572 * permission to perform all of the operations 1573 * against the target server as referenced above. 1574 * @param baseDN The base DN for the subtree to move. 1575 * @param sizeLimit The maximum number of entries to be moved. It 1576 * may be less than or equal to zero to indicate 1577 * that no client-side limit should be enforced 1578 * (although the server may still enforce its own 1579 * limit). 1580 * @param opPurposeControl An optional operation purpose request control 1581 * that may be included in all requests sent to the 1582 * source and target servers. 1583 * @param suppressRefInt Indicates whether to include a request control 1584 * causing referential integrity updates to be 1585 * suppressed on the source server. 1586 * @param listener An optional listener that may be invoked during 1587 * the course of moving entries from the source 1588 * server to the target server. 1589 * 1590 * @return An object with information about the result of the attempted 1591 * subtree move. 1592 */ 1593 @NotNull() 1594 private static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility( 1595 @Nullable final MoveSubtree tool, 1596 @NotNull final LDAPConnection sourceConnection, 1597 @NotNull final LDAPConnection targetConnection, 1598 @NotNull final String baseDN, final int sizeLimit, 1599 @Nullable final OperationPurposeRequestControl opPurposeControl, 1600 final boolean suppressRefInt, 1601 @Nullable final MoveSubtreeListener listener) 1602 { 1603 // Ensure that the subtree is currently accessible in both the source and 1604 // target servers. 1605 final MoveSubtreeResult initialAccessibilityResult = 1606 checkInitialAccessibility(sourceConnection, targetConnection, baseDN, 1607 opPurposeControl); 1608 if (initialAccessibilityResult != null) 1609 { 1610 return initialAccessibilityResult; 1611 } 1612 1613 1614 final StringBuilder errorMsg = new StringBuilder(); 1615 final StringBuilder adminMsg = new StringBuilder(); 1616 1617 final ReverseComparator<DN> reverseComparator = new ReverseComparator<>(); 1618 final TreeSet<DN> sourceEntryDNs = new TreeSet<>(reverseComparator); 1619 1620 final AtomicInteger entriesReadFromSource = new AtomicInteger(0); 1621 final AtomicInteger entriesAddedToTarget = new AtomicInteger(0); 1622 final AtomicInteger entriesDeletedFromSource = new AtomicInteger(0); 1623 final AtomicReference<ResultCode> resultCode = new AtomicReference<>(); 1624 1625 boolean sourceServerAltered = false; 1626 boolean targetServerAltered = false; 1627 1628 SubtreeAccessibilityState currentSourceState = 1629 SubtreeAccessibilityState.ACCESSIBLE; 1630 SubtreeAccessibilityState currentTargetState = 1631 SubtreeAccessibilityState.ACCESSIBLE; 1632 1633processingBlock: 1634 { 1635 // Identify the users authenticated on each connection. 1636 final String sourceUserDN; 1637 final String targetUserDN; 1638 try 1639 { 1640 sourceUserDN = getAuthenticatedUserDN(sourceConnection, true, 1641 opPurposeControl); 1642 targetUserDN = getAuthenticatedUserDN(targetConnection, false, 1643 opPurposeControl); 1644 } 1645 catch (final LDAPException le) 1646 { 1647 Debug.debugException(le); 1648 resultCode.compareAndSet(null, le.getResultCode()); 1649 append(le.getMessage(), errorMsg); 1650 break processingBlock; 1651 } 1652 1653 1654 // Make the subtree hidden on the target server. 1655 try 1656 { 1657 setAccessibility(targetConnection, false, baseDN, 1658 SubtreeAccessibilityState.HIDDEN, targetUserDN, opPurposeControl); 1659 currentTargetState = SubtreeAccessibilityState.HIDDEN; 1660 setInterruptMessage(tool, 1661 WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_HIDDEN.get(baseDN, 1662 targetConnection.getConnectedAddress(), 1663 targetConnection.getConnectedPort())); 1664 } 1665 catch (final LDAPException le) 1666 { 1667 Debug.debugException(le); 1668 resultCode.compareAndSet(null, le.getResultCode()); 1669 append(le.getMessage(), errorMsg); 1670 break processingBlock; 1671 } 1672 1673 1674 // Make the subtree read-only on the source server. 1675 try 1676 { 1677 setAccessibility(sourceConnection, true, baseDN, 1678 SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, sourceUserDN, 1679 opPurposeControl); 1680 currentSourceState = SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED; 1681 setInterruptMessage(tool, 1682 WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_READ_ONLY.get(baseDN, 1683 targetConnection.getConnectedAddress(), 1684 targetConnection.getConnectedPort(), 1685 sourceConnection.getConnectedAddress(), 1686 sourceConnection.getConnectedPort())); 1687 } 1688 catch (final LDAPException le) 1689 { 1690 Debug.debugException(le); 1691 resultCode.compareAndSet(null, le.getResultCode()); 1692 append(le.getMessage(), errorMsg); 1693 break processingBlock; 1694 } 1695 1696 1697 // Perform a search to find all entries in the target subtree, and include 1698 // a search listener that will add each entry to the target server as it 1699 // is returned from the source server. 1700 final Control[] searchControls; 1701 if (opPurposeControl == null) 1702 { 1703 searchControls = new Control[] 1704 { 1705 new DraftLDUPSubentriesRequestControl(true), 1706 new ManageDsaITRequestControl(true), 1707 new ReturnConflictEntriesRequestControl(true), 1708 new SoftDeletedEntryAccessRequestControl(true, true, false), 1709 new RealAttributesOnlyRequestControl(true) 1710 }; 1711 } 1712 else 1713 { 1714 searchControls = new Control[] 1715 { 1716 new DraftLDUPSubentriesRequestControl(true), 1717 new ManageDsaITRequestControl(true), 1718 new ReturnConflictEntriesRequestControl(true), 1719 new SoftDeletedEntryAccessRequestControl(true, true, false), 1720 new RealAttributesOnlyRequestControl(true), 1721 opPurposeControl 1722 }; 1723 } 1724 1725 final MoveSubtreeAccessibilitySearchListener searchListener = 1726 new MoveSubtreeAccessibilitySearchListener(tool, baseDN, 1727 sourceConnection, targetConnection, resultCode, errorMsg, 1728 entriesReadFromSource, entriesAddedToTarget, sourceEntryDNs, 1729 opPurposeControl, listener); 1730 final SearchRequest searchRequest = new SearchRequest( 1731 searchListener, searchControls, baseDN, SearchScope.SUB, 1732 DereferencePolicy.NEVER, sizeLimit, 0, false, 1733 Filter.createPresenceFilter("objectClass"), "*", "+"); 1734 1735 SearchResult searchResult; 1736 try 1737 { 1738 searchResult = sourceConnection.search(searchRequest); 1739 } 1740 catch (final LDAPSearchException lse) 1741 { 1742 Debug.debugException(lse); 1743 searchResult = lse.getSearchResult(); 1744 } 1745 1746 if (entriesAddedToTarget.get() > 0) 1747 { 1748 targetServerAltered = true; 1749 } 1750 1751 if (searchResult.getResultCode() != ResultCode.SUCCESS) 1752 { 1753 resultCode.compareAndSet(null, searchResult.getResultCode()); 1754 append( 1755 ERR_MOVE_SUBTREE_SEARCH_FAILED.get(baseDN, 1756 searchResult.getDiagnosticMessage()), 1757 errorMsg); 1758 1759 final AtomicInteger deleteCount = new AtomicInteger(0); 1760 if (targetServerAltered) 1761 { 1762 deleteEntries(targetConnection, false, sourceEntryDNs, 1763 opPurposeControl, false, null, deleteCount, resultCode, 1764 errorMsg); 1765 entriesAddedToTarget.addAndGet(0 - deleteCount.get()); 1766 if (entriesAddedToTarget.get() == 0) 1767 { 1768 targetServerAltered = false; 1769 } 1770 else 1771 { 1772 append(ERR_MOVE_SUBTREE_TARGET_NOT_DELETED_ADMIN_ACTION.get(baseDN), 1773 adminMsg); 1774 } 1775 } 1776 break processingBlock; 1777 } 1778 1779 // If an error occurred during add processing, then fail. 1780 if (resultCode.get() != null) 1781 { 1782 final AtomicInteger deleteCount = new AtomicInteger(0); 1783 if (targetServerAltered) 1784 { 1785 deleteEntries(targetConnection, false, sourceEntryDNs, 1786 opPurposeControl, false, null, deleteCount, resultCode, 1787 errorMsg); 1788 entriesAddedToTarget.addAndGet(0 - deleteCount.get()); 1789 if (entriesAddedToTarget.get() == 0) 1790 { 1791 targetServerAltered = false; 1792 } 1793 else 1794 { 1795 append(ERR_MOVE_SUBTREE_TARGET_NOT_DELETED_ADMIN_ACTION.get(baseDN), 1796 adminMsg); 1797 } 1798 } 1799 break processingBlock; 1800 } 1801 1802 1803 // Make the subtree read-only on the target server. 1804 try 1805 { 1806 setAccessibility(targetConnection, true, baseDN, 1807 SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, targetUserDN, 1808 opPurposeControl); 1809 currentTargetState = SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED; 1810 setInterruptMessage(tool, 1811 WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_READ_ONLY.get(baseDN, 1812 sourceConnection.getConnectedAddress(), 1813 sourceConnection.getConnectedPort(), 1814 targetConnection.getConnectedAddress(), 1815 targetConnection.getConnectedPort())); 1816 } 1817 catch (final LDAPException le) 1818 { 1819 Debug.debugException(le); 1820 resultCode.compareAndSet(null, le.getResultCode()); 1821 append(le.getMessage(), errorMsg); 1822 break processingBlock; 1823 } 1824 1825 1826 // Make the subtree hidden on the source server. 1827 try 1828 { 1829 setAccessibility(sourceConnection, true, baseDN, 1830 SubtreeAccessibilityState.HIDDEN, sourceUserDN, 1831 opPurposeControl); 1832 currentSourceState = SubtreeAccessibilityState.HIDDEN; 1833 setInterruptMessage(tool, 1834 WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_HIDDEN.get(baseDN, 1835 sourceConnection.getConnectedAddress(), 1836 sourceConnection.getConnectedPort(), 1837 targetConnection.getConnectedAddress(), 1838 targetConnection.getConnectedPort())); 1839 } 1840 catch (final LDAPException le) 1841 { 1842 Debug.debugException(le); 1843 resultCode.compareAndSet(null, le.getResultCode()); 1844 append(le.getMessage(), errorMsg); 1845 break processingBlock; 1846 } 1847 1848 1849 // Make the subtree accessible on the target server. 1850 try 1851 { 1852 setAccessibility(targetConnection, true, baseDN, 1853 SubtreeAccessibilityState.ACCESSIBLE, targetUserDN, 1854 opPurposeControl); 1855 currentTargetState = SubtreeAccessibilityState.ACCESSIBLE; 1856 setInterruptMessage(tool, 1857 WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_ACCESSIBLE.get(baseDN, 1858 sourceConnection.getConnectedAddress(), 1859 sourceConnection.getConnectedPort(), 1860 targetConnection.getConnectedAddress(), 1861 targetConnection.getConnectedPort())); 1862 } 1863 catch (final LDAPException le) 1864 { 1865 Debug.debugException(le); 1866 resultCode.compareAndSet(null, le.getResultCode()); 1867 append(le.getMessage(), errorMsg); 1868 break processingBlock; 1869 } 1870 1871 1872 // Delete each of the entries in the source server. The map should 1873 // already be sorted in reverse order (as a result of the comparator used 1874 // when creating it), so it will guarantee children are deleted before 1875 // their parents. 1876 final boolean deleteSuccessful = deleteEntries(sourceConnection, true, 1877 sourceEntryDNs, opPurposeControl, suppressRefInt, listener, 1878 entriesDeletedFromSource, resultCode, errorMsg); 1879 sourceServerAltered = (entriesDeletedFromSource.get() != 0); 1880 if (! deleteSuccessful) 1881 { 1882 append(ERR_MOVE_SUBTREE_SOURCE_NOT_DELETED_ADMIN_ACTION.get(baseDN), 1883 adminMsg); 1884 break processingBlock; 1885 } 1886 1887 1888 // Make the subtree accessible on the source server. 1889 try 1890 { 1891 setAccessibility(sourceConnection, true, baseDN, 1892 SubtreeAccessibilityState.ACCESSIBLE, sourceUserDN, 1893 opPurposeControl); 1894 currentSourceState = SubtreeAccessibilityState.ACCESSIBLE; 1895 setInterruptMessage(tool, null); 1896 } 1897 catch (final LDAPException le) 1898 { 1899 Debug.debugException(le); 1900 resultCode.compareAndSet(null, le.getResultCode()); 1901 append(le.getMessage(), errorMsg); 1902 break processingBlock; 1903 } 1904 } 1905 1906 1907 // If the source server was left in a state other than accessible, then 1908 // see if we can safely change it back. If it's left in any state other 1909 // then accessible, then generate an admin action message. 1910 if (currentSourceState != SubtreeAccessibilityState.ACCESSIBLE) 1911 { 1912 if (! sourceServerAltered) 1913 { 1914 try 1915 { 1916 setAccessibility(sourceConnection, true, baseDN, 1917 SubtreeAccessibilityState.ACCESSIBLE, null, opPurposeControl); 1918 currentSourceState = SubtreeAccessibilityState.ACCESSIBLE; 1919 } 1920 catch (final LDAPException le) 1921 { 1922 Debug.debugException(le); 1923 } 1924 } 1925 1926 if (currentSourceState != SubtreeAccessibilityState.ACCESSIBLE) 1927 { 1928 append( 1929 ERR_MOVE_SUBTREE_SOURCE_LEFT_INACCESSIBLE.get( 1930 currentSourceState, baseDN), 1931 adminMsg); 1932 } 1933 } 1934 1935 1936 // If the target server was left in a state other than accessible, then 1937 // see if we can safely change it back. If it's left in any state other 1938 // then accessible, then generate an admin action message. 1939 if (currentTargetState != SubtreeAccessibilityState.ACCESSIBLE) 1940 { 1941 if (! targetServerAltered) 1942 { 1943 try 1944 { 1945 setAccessibility(targetConnection, false, baseDN, 1946 SubtreeAccessibilityState.ACCESSIBLE, null, opPurposeControl); 1947 currentTargetState = SubtreeAccessibilityState.ACCESSIBLE; 1948 } 1949 catch (final LDAPException le) 1950 { 1951 Debug.debugException(le); 1952 } 1953 } 1954 1955 if (currentTargetState != SubtreeAccessibilityState.ACCESSIBLE) 1956 { 1957 append( 1958 ERR_MOVE_SUBTREE_TARGET_LEFT_INACCESSIBLE.get( 1959 currentTargetState, baseDN), 1960 adminMsg); 1961 } 1962 } 1963 1964 1965 // Construct the result to return to the client. 1966 resultCode.compareAndSet(null, ResultCode.SUCCESS); 1967 1968 final String errorMessage; 1969 if (errorMsg.length() > 0) 1970 { 1971 errorMessage = errorMsg.toString(); 1972 } 1973 else 1974 { 1975 errorMessage = null; 1976 } 1977 1978 final String adminActionRequired; 1979 if (adminMsg.length() > 0) 1980 { 1981 adminActionRequired = adminMsg.toString(); 1982 } 1983 else 1984 { 1985 adminActionRequired = null; 1986 } 1987 1988 return new MoveSubtreeResult(resultCode.get(), errorMessage, 1989 adminActionRequired, sourceServerAltered, targetServerAltered, 1990 entriesReadFromSource.get(), entriesAddedToTarget.get(), 1991 entriesDeletedFromSource.get()); 1992 } 1993 1994 1995 1996 /** 1997 * Retrieves the DN of the user authenticated on the provided connection. It 1998 * will first try to look at the last successful bind request processed on the 1999 * connection, and will fall back to using the "Who Am I?" extended request. 2000 * 2001 * @param connection The connection for which to make the 2002 * determination. 2003 * @param isSource Indicates whether the connection is to the source 2004 * or target server. 2005 * @param opPurposeControl An optional operation purpose request control 2006 * that may be included in the request. 2007 * 2008 * @return The DN of the user authenticated on the provided connection, or 2009 * {@code null} if the connection is not authenticated. 2010 * 2011 * @throws LDAPException If a problem is encountered while making the 2012 * determination. 2013 */ 2014 @Nullable() 2015 private static String getAuthenticatedUserDN( 2016 @NotNull final LDAPConnection connection, 2017 final boolean isSource, 2018 @Nullable final OperationPurposeRequestControl opPurposeControl) 2019 throws LDAPException 2020 { 2021 final BindRequest bindRequest = 2022 InternalSDKHelper.getLastBindRequest(connection); 2023 if ((bindRequest != null) && (bindRequest instanceof SimpleBindRequest)) 2024 { 2025 final SimpleBindRequest r = (SimpleBindRequest) bindRequest; 2026 return r.getBindDN(); 2027 } 2028 2029 2030 final Control[] controls; 2031 if (opPurposeControl == null) 2032 { 2033 controls = StaticUtils.NO_CONTROLS; 2034 } 2035 else 2036 { 2037 controls = new Control[] 2038 { 2039 opPurposeControl 2040 }; 2041 } 2042 2043 final String connectionName = 2044 isSource 2045 ? INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get() 2046 : INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(); 2047 2048 final WhoAmIExtendedResult whoAmIResult; 2049 try 2050 { 2051 whoAmIResult = (WhoAmIExtendedResult) 2052 connection.processExtendedOperation( 2053 new WhoAmIExtendedRequest(controls)); 2054 } 2055 catch (final LDAPException le) 2056 { 2057 Debug.debugException(le); 2058 throw new LDAPException(le.getResultCode(), 2059 ERR_MOVE_SUBTREE_ERROR_INVOKING_WHO_AM_I.get(connectionName, 2060 StaticUtils.getExceptionMessage(le)), 2061 le); 2062 } 2063 2064 if (whoAmIResult.getResultCode() != ResultCode.SUCCESS) 2065 { 2066 throw new LDAPException(whoAmIResult.getResultCode(), 2067 ERR_MOVE_SUBTREE_ERROR_INVOKING_WHO_AM_I.get(connectionName, 2068 whoAmIResult.getDiagnosticMessage())); 2069 } 2070 2071 final String authzID = whoAmIResult.getAuthorizationID(); 2072 if ((authzID != null) && authzID.startsWith("dn:")) 2073 { 2074 return authzID.substring(3); 2075 } 2076 else 2077 { 2078 throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, 2079 ERR_MOVE_SUBTREE_CANNOT_IDENTIFY_CONNECTED_USER.get(connectionName)); 2080 } 2081 } 2082 2083 2084 2085 /** 2086 * Ensures that the specified subtree is accessible in both the source and 2087 * target servers. If it is not accessible, then it may indicate that another 2088 * administrative operation is in progress for the subtree, or that a previous 2089 * move-subtree operation was interrupted before it could complete. 2090 * 2091 * @param sourceConnection The connection to use to communicate with the 2092 * source directory server. 2093 * @param targetConnection The connection to use to communicate with the 2094 * target directory server. 2095 * @param baseDN The base DN for which to verify accessibility. 2096 * @param opPurposeControl An optional operation purpose request control 2097 * that may be included in the requests. 2098 * 2099 * @return {@code null} if the specified subtree is accessible in both the 2100 * source and target servers, or a non-{@code null} object with the 2101 * result that should be used if there is an accessibility problem 2102 * with the subtree on the source and/or target server. 2103 */ 2104 @Nullable() 2105 private static MoveSubtreeResult checkInitialAccessibility( 2106 @NotNull final LDAPConnection sourceConnection, 2107 @NotNull final LDAPConnection targetConnection, 2108 @NotNull final String baseDN, 2109 @Nullable final OperationPurposeRequestControl opPurposeControl) 2110 { 2111 final DN parsedBaseDN; 2112 try 2113 { 2114 parsedBaseDN = new DN(baseDN); 2115 } 2116 catch (final Exception e) 2117 { 2118 Debug.debugException(e); 2119 return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX, 2120 ERR_MOVE_SUBTREE_CANNOT_PARSE_BASE_DN.get(baseDN, 2121 StaticUtils.getExceptionMessage(e)), 2122 null, false, false, 0, 0, 0); 2123 } 2124 2125 final Control[] controls; 2126 if (opPurposeControl == null) 2127 { 2128 controls = StaticUtils.NO_CONTROLS; 2129 } 2130 else 2131 { 2132 controls = new Control[] 2133 { 2134 opPurposeControl 2135 }; 2136 } 2137 2138 2139 // Get the restrictions from the source server. If there are any, then 2140 // make sure that nothing in the hierarchy of the base DN is non-accessible. 2141 final GetSubtreeAccessibilityExtendedResult sourceResult; 2142 try 2143 { 2144 sourceResult = (GetSubtreeAccessibilityExtendedResult) 2145 sourceConnection.processExtendedOperation( 2146 new GetSubtreeAccessibilityExtendedRequest(controls)); 2147 if (sourceResult.getResultCode() != ResultCode.SUCCESS) 2148 { 2149 throw new LDAPException(sourceResult); 2150 } 2151 } 2152 catch (final LDAPException le) 2153 { 2154 Debug.debugException(le); 2155 return new MoveSubtreeResult(le.getResultCode(), 2156 ERR_MOVE_SUBTREE_CANNOT_GET_ACCESSIBILITY_STATE.get(baseDN, 2157 INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), 2158 le.getMessage()), 2159 null, false, false, 0, 0, 0); 2160 } 2161 2162 boolean sourceMatch = false; 2163 String sourceMessage = null; 2164 SubtreeAccessibilityRestriction sourceRestriction = null; 2165 final List<SubtreeAccessibilityRestriction> sourceRestrictions = 2166 sourceResult.getAccessibilityRestrictions(); 2167 if (sourceRestrictions != null) 2168 { 2169 for (final SubtreeAccessibilityRestriction r : sourceRestrictions) 2170 { 2171 if (r.getAccessibilityState() == SubtreeAccessibilityState.ACCESSIBLE) 2172 { 2173 continue; 2174 } 2175 2176 final DN restrictionDN; 2177 try 2178 { 2179 restrictionDN = new DN(r.getSubtreeBaseDN()); 2180 } 2181 catch (final Exception e) 2182 { 2183 Debug.debugException(e); 2184 return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX, 2185 ERR_MOVE_SUBTREE_CANNOT_PARSE_RESTRICTION_BASE_DN.get( 2186 r.getSubtreeBaseDN(), 2187 INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), 2188 r.toString(), StaticUtils.getExceptionMessage(e)), 2189 null, false, false, 0, 0, 0); 2190 } 2191 2192 if (restrictionDN.equals(parsedBaseDN)) 2193 { 2194 sourceMatch = true; 2195 sourceRestriction = r; 2196 sourceMessage = ERR_MOVE_SUBTREE_NOT_ACCESSIBLE.get(baseDN, 2197 INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), 2198 r.getAccessibilityState().getStateName()); 2199 break; 2200 } 2201 else if (restrictionDN.isAncestorOf(parsedBaseDN, false)) 2202 { 2203 sourceRestriction = r; 2204 sourceMessage = ERR_MOVE_SUBTREE_WITHIN_UNACCESSIBLE_TREE.get(baseDN, 2205 INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), 2206 r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); 2207 break; 2208 } 2209 else if (restrictionDN.isDescendantOf(parsedBaseDN, false)) 2210 { 2211 sourceRestriction = r; 2212 sourceMessage = ERR_MOVE_SUBTREE_CONTAINS_UNACCESSIBLE_TREE.get( 2213 baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), 2214 r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); 2215 break; 2216 } 2217 } 2218 } 2219 2220 2221 // Get the restrictions from the target server. If there are any, then 2222 // make sure that nothing in the hierarchy of the base DN is non-accessible. 2223 final GetSubtreeAccessibilityExtendedResult targetResult; 2224 try 2225 { 2226 targetResult = (GetSubtreeAccessibilityExtendedResult) 2227 targetConnection.processExtendedOperation( 2228 new GetSubtreeAccessibilityExtendedRequest(controls)); 2229 if (targetResult.getResultCode() != ResultCode.SUCCESS) 2230 { 2231 throw new LDAPException(targetResult); 2232 } 2233 } 2234 catch (final LDAPException le) 2235 { 2236 Debug.debugException(le); 2237 return new MoveSubtreeResult(le.getResultCode(), 2238 ERR_MOVE_SUBTREE_CANNOT_GET_ACCESSIBILITY_STATE.get(baseDN, 2239 INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), 2240 le.getMessage()), 2241 null, false, false, 0, 0, 0); 2242 } 2243 2244 boolean targetMatch = false; 2245 String targetMessage = null; 2246 SubtreeAccessibilityRestriction targetRestriction = null; 2247 final List<SubtreeAccessibilityRestriction> targetRestrictions = 2248 targetResult.getAccessibilityRestrictions(); 2249 if (targetRestrictions != null) 2250 { 2251 for (final SubtreeAccessibilityRestriction r : targetRestrictions) 2252 { 2253 if (r.getAccessibilityState() == SubtreeAccessibilityState.ACCESSIBLE) 2254 { 2255 continue; 2256 } 2257 2258 final DN restrictionDN; 2259 try 2260 { 2261 restrictionDN = new DN(r.getSubtreeBaseDN()); 2262 } 2263 catch (final Exception e) 2264 { 2265 Debug.debugException(e); 2266 return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX, 2267 ERR_MOVE_SUBTREE_CANNOT_PARSE_RESTRICTION_BASE_DN.get( 2268 r.getSubtreeBaseDN(), 2269 INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), 2270 r.toString(), StaticUtils.getExceptionMessage(e)), 2271 null, false, false, 0, 0, 0); 2272 } 2273 2274 if (restrictionDN.equals(parsedBaseDN)) 2275 { 2276 targetMatch = true; 2277 targetRestriction = r; 2278 targetMessage = ERR_MOVE_SUBTREE_NOT_ACCESSIBLE.get(baseDN, 2279 INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), 2280 r.getAccessibilityState().getStateName()); 2281 break; 2282 } 2283 else if (restrictionDN.isAncestorOf(parsedBaseDN, false)) 2284 { 2285 targetRestriction = r; 2286 targetMessage = ERR_MOVE_SUBTREE_WITHIN_UNACCESSIBLE_TREE.get(baseDN, 2287 INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), 2288 r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); 2289 break; 2290 } 2291 else if (restrictionDN.isDescendantOf(parsedBaseDN, false)) 2292 { 2293 targetRestriction = r; 2294 targetMessage = ERR_MOVE_SUBTREE_CONTAINS_UNACCESSIBLE_TREE.get( 2295 baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), 2296 r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); 2297 break; 2298 } 2299 } 2300 } 2301 2302 2303 // If both the source and target servers are available, then we don't need 2304 // to do anything else. 2305 if ((sourceRestriction == null) && (targetRestriction == null)) 2306 { 2307 return null; 2308 } 2309 2310 2311 // If we got a match for both the source and target subtrees, then there's a 2312 // good chance that condition results from an interrupted earlier attempt at 2313 // running move-subtree. If that's the case, then see if we can provide 2314 // specific advice about how to recover. 2315 if (sourceMatch || targetMatch) 2316 { 2317 // If the source is read-only and the target is hidden, then it was 2318 // probably in the process of adding entries to the target. Recommend 2319 // deleting all entries in the target subtree and making both subtrees 2320 // accessible before running again. 2321 if ((sourceRestriction != null) && 2322 sourceRestriction.getAccessibilityState().isReadOnly() && 2323 (targetRestriction != null) && 2324 targetRestriction.getAccessibilityState().isHidden()) 2325 { 2326 return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM, 2327 ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_ADDS.get(baseDN, 2328 sourceConnection.getConnectedAddress(), 2329 sourceConnection.getConnectedPort(), 2330 targetConnection.getConnectedAddress(), 2331 targetConnection.getConnectedPort()), 2332 ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_ADDS_ADMIN_MSG.get(), 2333 false, false, 0, 0, 0); 2334 } 2335 2336 2337 // If the source is hidden and the target is accessible, then it was 2338 // probably in the process of deleting entries from the source. Recommend 2339 // deleting all entries in the source subtree and making the source 2340 // subtree accessible. There shouldn't be a need to run again. 2341 if ((sourceRestriction != null) && 2342 sourceRestriction.getAccessibilityState().isHidden() && 2343 (targetRestriction == null)) 2344 { 2345 return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM, 2346 ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_DELETES.get(baseDN, 2347 sourceConnection.getConnectedAddress(), 2348 sourceConnection.getConnectedPort(), 2349 targetConnection.getConnectedAddress(), 2350 targetConnection.getConnectedPort()), 2351 ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_DELETES_ADMIN_MSG.get(), 2352 false, false, 0, 0, 0); 2353 } 2354 } 2355 2356 2357 // If we've made it here, then we're in a situation we don't recognize. 2358 // Provide general information about the current state of the subtree and 2359 // recommend that the user contact support if they need assistance. 2360 final StringBuilder details = new StringBuilder(); 2361 if (sourceMessage != null) 2362 { 2363 details.append(sourceMessage); 2364 } 2365 if (targetMessage != null) 2366 { 2367 append(targetMessage, details); 2368 } 2369 return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM, 2370 ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED.get(baseDN, 2371 sourceConnection.getConnectedAddress(), 2372 sourceConnection.getConnectedPort(), 2373 targetConnection.getConnectedAddress(), 2374 targetConnection.getConnectedPort(), details.toString()), 2375 null, false, false, 0, 0, 0); 2376 } 2377 2378 2379 2380 /** 2381 * Updates subtree accessibility in a server. 2382 * 2383 * @param connection The connection to the server in which the 2384 * accessibility state should be applied. 2385 * @param isSource Indicates whether the connection is to the source 2386 * or target server. 2387 * @param baseDN The base DN for the subtree to move. 2388 * @param state The accessibility state to apply. 2389 * @param bypassDN The DN of a user that will be allowed to bypass 2390 * accessibility restrictions. It may be 2391 * {@code null} if none is needed. 2392 * @param opPurposeControl An optional operation purpose request control 2393 * that may be included in the request. 2394 * 2395 * @throws LDAPException If a problem is encountered while attempting to set 2396 * the accessibility state for the subtree. 2397 */ 2398 private static void setAccessibility( 2399 @NotNull final LDAPConnection connection, 2400 final boolean isSource, 2401 @NotNull final String baseDN, 2402 @NotNull final SubtreeAccessibilityState state, 2403 @Nullable final String bypassDN, 2404 @Nullable final OperationPurposeRequestControl opPurposeControl) 2405 throws LDAPException 2406 { 2407 final String connectionName = 2408 isSource 2409 ? INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get() 2410 : INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(); 2411 2412 final Control[] controls; 2413 if (opPurposeControl == null) 2414 { 2415 controls = StaticUtils.NO_CONTROLS; 2416 } 2417 else 2418 { 2419 controls = new Control[] 2420 { 2421 opPurposeControl 2422 }; 2423 } 2424 2425 final SetSubtreeAccessibilityExtendedRequest request; 2426 switch (state) 2427 { 2428 case ACCESSIBLE: 2429 request = SetSubtreeAccessibilityExtendedRequest. 2430 createSetAccessibleRequest(baseDN, controls); 2431 break; 2432 case READ_ONLY_BIND_ALLOWED: 2433 request = SetSubtreeAccessibilityExtendedRequest. 2434 createSetReadOnlyRequest(baseDN, true, bypassDN, controls); 2435 break; 2436 case READ_ONLY_BIND_DENIED: 2437 request = SetSubtreeAccessibilityExtendedRequest. 2438 createSetReadOnlyRequest(baseDN, false, bypassDN, controls); 2439 break; 2440 case HIDDEN: 2441 request = SetSubtreeAccessibilityExtendedRequest. 2442 createSetHiddenRequest(baseDN, bypassDN, controls); 2443 break; 2444 default: 2445 throw new LDAPException(ResultCode.PARAM_ERROR, 2446 ERR_MOVE_SUBTREE_UNSUPPORTED_ACCESSIBILITY_STATE.get( 2447 state.getStateName(), baseDN, connectionName)); 2448 } 2449 2450 LDAPResult result; 2451 try 2452 { 2453 result = connection.processExtendedOperation(request); 2454 } 2455 catch (final LDAPException le) 2456 { 2457 Debug.debugException(le); 2458 result = le.toLDAPResult(); 2459 } 2460 2461 if (result.getResultCode() != ResultCode.SUCCESS) 2462 { 2463 throw new LDAPException(result.getResultCode(), 2464 ERR_MOVE_SUBTREE_ERROR_SETTING_ACCESSIBILITY.get( 2465 state.getStateName(), baseDN, connectionName, 2466 result.getDiagnosticMessage())); 2467 } 2468 } 2469 2470 2471 2472 /** 2473 * Sets the interrupt message for the given tool, if one was provided. 2474 * 2475 * @param tool The tool for which to set the interrupt message. It may 2476 * be {@code null} if no action should be taken. 2477 * @param message The interrupt message to set. It may be {@code null} if 2478 * an existing interrupt message should be cleared. 2479 */ 2480 static void setInterruptMessage(@Nullable final MoveSubtree tool, 2481 @Nullable final String message) 2482 { 2483 if (tool != null) 2484 { 2485 tool.interruptMessage = message; 2486 } 2487 } 2488 2489 2490 2491 /** 2492 * Deletes a specified set of entries from the indicated server. 2493 * 2494 * @param connection The connection to use to communicate with the 2495 * server. 2496 * @param isSource Indicates whether the connection is to the source 2497 * or target server. 2498 * @param entryDNs The set of DNs of the entries to be deleted. 2499 * @param opPurposeControl An optional operation purpose request control 2500 * that may be included in the requests. 2501 * @param suppressRefInt Indicates whether to include a request control 2502 * causing referential integrity updates to be 2503 * suppressed on the source server. 2504 * @param listener An optional listener that may be invoked during 2505 * the course of moving entries from the source 2506 * server to the target server. 2507 * @param deleteCount A counter to increment for each delete operation 2508 * processed. 2509 * @param resultCode A reference to the result code to use for the 2510 * move subtree operation. 2511 * @param errorMsg A buffer to which any appropriate error messages 2512 * may be appended. 2513 * 2514 * @return {@code true} if the delete was completely successful, or 2515 * {@code false} if any errors were encountered. 2516 */ 2517 private static boolean deleteEntries( 2518 @NotNull final LDAPConnection connection, 2519 final boolean isSource, 2520 @NotNull final TreeSet<DN> entryDNs, 2521 @Nullable final OperationPurposeRequestControl opPurposeControl, 2522 final boolean suppressRefInt, 2523 @Nullable final MoveSubtreeListener listener, 2524 @NotNull final AtomicInteger deleteCount, 2525 @NotNull final AtomicReference<ResultCode> resultCode, 2526 @NotNull final StringBuilder errorMsg) 2527 { 2528 final ArrayList<Control> deleteControlList = new ArrayList<>(3); 2529 deleteControlList.add(new ManageDsaITRequestControl(true)); 2530 if (opPurposeControl != null) 2531 { 2532 deleteControlList.add(opPurposeControl); 2533 } 2534 if (suppressRefInt) 2535 { 2536 deleteControlList.add( 2537 new SuppressReferentialIntegrityUpdatesRequestControl(false)); 2538 } 2539 2540 final Control[] deleteControls = new Control[deleteControlList.size()]; 2541 deleteControlList.toArray(deleteControls); 2542 2543 boolean successful = true; 2544 for (final DN dn : entryDNs) 2545 { 2546 if (isSource && (listener != null)) 2547 { 2548 try 2549 { 2550 listener.doPreDeleteProcessing(dn); 2551 } 2552 catch (final Exception e) 2553 { 2554 Debug.debugException(e); 2555 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 2556 append( 2557 ERR_MOVE_SUBTREE_PRE_DELETE_FAILURE.get(dn.toString(), 2558 StaticUtils.getExceptionMessage(e)), 2559 errorMsg); 2560 successful = false; 2561 continue; 2562 } 2563 } 2564 2565 LDAPResult deleteResult; 2566 try 2567 { 2568 deleteResult = connection.delete(new DeleteRequest(dn, deleteControls)); 2569 } 2570 catch (final LDAPException le) 2571 { 2572 Debug.debugException(le); 2573 deleteResult = le.toLDAPResult(); 2574 } 2575 2576 if (deleteResult.getResultCode() == ResultCode.SUCCESS) 2577 { 2578 deleteCount.incrementAndGet(); 2579 } 2580 else 2581 { 2582 resultCode.compareAndSet(null, deleteResult.getResultCode()); 2583 append( 2584 ERR_MOVE_SUBTREE_DELETE_FAILURE.get( 2585 dn.toString(), 2586 deleteResult.getDiagnosticMessage()), 2587 errorMsg); 2588 successful = false; 2589 continue; 2590 } 2591 2592 if (isSource && (listener != null)) 2593 { 2594 try 2595 { 2596 listener.doPostDeleteProcessing(dn); 2597 } 2598 catch (final Exception e) 2599 { 2600 Debug.debugException(e); 2601 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 2602 append( 2603 ERR_MOVE_SUBTREE_POST_DELETE_FAILURE.get(dn.toString(), 2604 StaticUtils.getExceptionMessage(e)), 2605 errorMsg); 2606 successful = false; 2607 } 2608 } 2609 } 2610 2611 return successful; 2612 } 2613 2614 2615 2616 /** 2617 * Appends the provided message to the given buffer. If the buffer is not 2618 * empty, then it will insert two spaces before the message. 2619 * 2620 * @param message The message to be appended to the buffer. 2621 * @param buffer The buffer to which the message should be appended. 2622 */ 2623 static void append(@Nullable final String message, 2624 @NotNull final StringBuilder buffer) 2625 { 2626 if (message != null) 2627 { 2628 if (buffer.length() > 0) 2629 { 2630 buffer.append(" "); 2631 } 2632 2633 buffer.append(message); 2634 } 2635 } 2636 2637 2638 2639 /** 2640 * {@inheritDoc} 2641 */ 2642 @Override() 2643 public void handleUnsolicitedNotification( 2644 @NotNull final LDAPConnection connection, 2645 @NotNull final ExtendedResult notification) 2646 { 2647 wrapOut(0, 79, 2648 INFO_MOVE_SUBTREE_UNSOLICITED_NOTIFICATION.get(notification.getOID(), 2649 connection.getConnectionName(), notification.getResultCode(), 2650 notification.getDiagnosticMessage())); 2651 } 2652 2653 2654 2655 /** 2656 * {@inheritDoc} 2657 */ 2658 @Override() 2659 @NotNull() 2660 public ReadOnlyEntry doPreAddProcessing(@NotNull final ReadOnlyEntry entry) 2661 { 2662 // No processing required. 2663 return entry; 2664 } 2665 2666 2667 2668 /** 2669 * {@inheritDoc} 2670 */ 2671 @Override() 2672 public void doPostAddProcessing(@NotNull final ReadOnlyEntry entry) 2673 { 2674 wrapOut(0, 79, INFO_MOVE_SUBTREE_ADD_SUCCESSFUL.get(entry.getDN())); 2675 } 2676 2677 2678 2679 /** 2680 * {@inheritDoc} 2681 */ 2682 @Override() 2683 public void doPreDeleteProcessing(@NotNull final DN entryDN) 2684 { 2685 // No processing required. 2686 } 2687 2688 2689 2690 /** 2691 * {@inheritDoc} 2692 */ 2693 @Override() 2694 public void doPostDeleteProcessing(@NotNull final DN entryDN) 2695 { 2696 wrapOut(0, 79, INFO_MOVE_SUBTREE_DELETE_SUCCESSFUL.get(entryDN.toString())); 2697 } 2698 2699 2700 2701 /** 2702 * {@inheritDoc} 2703 */ 2704 @Override() 2705 protected boolean registerShutdownHook() 2706 { 2707 return true; 2708 } 2709 2710 2711 2712 /** 2713 * {@inheritDoc} 2714 */ 2715 @Override() 2716 protected void doShutdownHookProcessing(@Nullable final ResultCode resultCode) 2717 { 2718 if (resultCode != null) 2719 { 2720 // The tool exited normally, so we don't need to do anything. 2721 return; 2722 } 2723 2724 // If there is an interrupt message, then display it. 2725 wrapErr(0, 79, interruptMessage); 2726 } 2727 2728 2729 2730 /** 2731 * {@inheritDoc} 2732 */ 2733 @Override() 2734 @NotNull() 2735 public LinkedHashMap<String[],String> getExampleUsages() 2736 { 2737 final LinkedHashMap<String[],String> exampleMap = 2738 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 2739 2740 final String[] args = 2741 { 2742 "--sourceHostname", "ds1.example.com", 2743 "--sourcePort", "389", 2744 "--sourceBindDN", "uid=admin,dc=example,dc=com", 2745 "--sourceBindPassword", "password", 2746 "--targetHostname", "ds2.example.com", 2747 "--targetPort", "389", 2748 "--targetBindDN", "uid=admin,dc=example,dc=com", 2749 "--targetBindPassword", "password", 2750 "--baseDN", "cn=small subtree,dc=example,dc=com", 2751 "--sizeLimit", "100", 2752 "--purpose", "Migrate a small subtree from ds1 to ds2" 2753 }; 2754 exampleMap.put(args, INFO_MOVE_SUBTREE_EXAMPLE_DESCRIPTION.get()); 2755 2756 return exampleMap; 2757 } 2758}