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