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