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