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