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