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