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