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