All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldap.sdk.unboundidds.MoveSubtree Maven / Gradle / Ivy

/*
 * Copyright 2012-2017 UnboundID Corp.
 * All Rights Reserved.
 */
/*
 * Copyright (C) 2015-2017 UnboundID Corp.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldap.sdk.unboundidds;



import java.io.OutputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import com.unboundid.asn1.ASN1OctetString;
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.Control;
import com.unboundid.ldap.sdk.DeleteRequest;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.ExtendedResult;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.InternalSDKHelper;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.ReadOnlyEntry;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.RootDSE;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler;
import com.unboundid.ldap.sdk.Version;
import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
import com.unboundid.ldap.sdk.controls.SubentriesRequestControl;
import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedRequest;
import com.unboundid.ldap.sdk.extensions.WhoAmIExtendedResult;
import com.unboundid.ldap.sdk.unboundidds.controls.
            InteractiveTransactionSpecificationRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            InteractiveTransactionSpecificationResponseControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            OperationPurposeRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            RealAttributesOnlyRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            ReturnConflictEntriesRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            SoftDeletedEntryAccessRequestControl;
import com.unboundid.ldap.sdk.unboundidds.controls.
            SuppressReferentialIntegrityUpdatesRequestControl;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            EndInteractiveTransactionExtendedRequest;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            GetSubtreeAccessibilityExtendedRequest;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            GetSubtreeAccessibilityExtendedResult;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            SetSubtreeAccessibilityExtendedRequest;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            StartInteractiveTransactionExtendedRequest;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            StartInteractiveTransactionExtendedResult;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            SubtreeAccessibilityRestriction;
import com.unboundid.ldap.sdk.unboundidds.extensions.
            SubtreeAccessibilityState;
import com.unboundid.util.Debug;
import com.unboundid.util.MultiServerLDAPCommandLineTool;
import com.unboundid.util.ReverseComparator;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.DNArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.IntegerArgument;
import com.unboundid.util.args.StringArgument;

import static com.unboundid.ldap.sdk.unboundidds.UnboundIDDSMessages.*;



/**
 * This class provides a utility that may be used to move a single entry or a
 * small subtree of entries from one server to another.
 * 
*
* NOTE: This class is part of the Commercial Edition of the UnboundID * LDAP SDK for Java. It is not available for use in applications that * include only the Standard Edition of the LDAP SDK, and is not supported for * use in conjunction with non-UnboundID products. *
*/ @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) public final class MoveSubtree extends MultiServerLDAPCommandLineTool implements UnsolicitedNotificationHandler, MoveSubtreeListener { /** * The name of the attribute that appears in the root DSE of Ping * Identity, UnboundID, and Alcatel-Lucent 8661 Directory Server instances to * provide a unique identifier that will be generated every time the server * starts. */ private static final String ATTR_STARTUP_UUID = "startupUUID"; // The argument used to indicate whether to operate in verbose mode. private BooleanArgument verbose = null; // The argument used to specify the base DNs of the subtrees to move. private DNArgument baseDN = null; // The argument used to specify a file with base DNs of the subtrees to move. private FileArgument baseDNFile = null; // The argument used to specify the maximum number of entries to move. private IntegerArgument sizeLimit = null; // A message that will be displayed if the tool is interrupted. private volatile String interruptMessage = null; // The argument used to specify the purpose for the move. private StringArgument purpose = null; /** * Parse the provided command line arguments and perform the appropriate * processing. * * @param args The command line arguments provided to this program. */ public static void main(final String... args) { final ResultCode rc = main(args, System.out, System.err); if (rc != ResultCode.SUCCESS) { System.exit(Math.max(rc.intValue(), 255)); } } /** * Parse the provided command line arguments and perform the appropriate * processing. * * @param args The command line arguments provided to this program. * @param out The output stream to which standard out should be written. * It may be {@code null} if output should be suppressed. * @param err The output stream to which standard error should be written. * It may be {@code null} if error messages should be * suppressed. * * @return A result code indicating whether the processing was successful. */ public static ResultCode main(final String[] args, final OutputStream out, final OutputStream err) { final MoveSubtree moveSubtree = new MoveSubtree(out, err); return moveSubtree.runTool(args); } /** * Creates a new instance of this tool with the provided output and error * streams. * * @param out The output stream to which standard out should be written. It * may be {@code null} if output should be suppressed. * @param err The output stream to which standard error should be written. * It may be {@code null} if error messages should be suppressed. */ public MoveSubtree(final OutputStream out, final OutputStream err) { super(out, err, new String[] { "source", "target" }, null); } /** * {@inheritDoc} */ @Override() public String getToolName() { return "move-subtree"; } /** * {@inheritDoc} */ @Override() public String getToolDescription() { return INFO_MOVE_SUBTREE_TOOL_DESCRIPTION.get(); } /** * {@inheritDoc} */ @Override() public String getToolVersion() { return Version.NUMERIC_VERSION_STRING; } /** * {@inheritDoc} */ @Override() public void addNonLDAPArguments(final ArgumentParser parser) throws ArgumentException { baseDN = new DNArgument('b', "baseDN", false, 0, INFO_MOVE_SUBTREE_ARG_BASE_DN_PLACEHOLDER.get(), INFO_MOVE_SUBTREE_ARG_BASE_DN_DESCRIPTION.get()); baseDN.addLongIdentifier("entryDN"); parser.addArgument(baseDN); baseDNFile = new FileArgument('f', "baseDNFile", false, 1, INFO_MOVE_SUBTREE_ARG_BASE_DN_FILE_PLACEHOLDER.get(), INFO_MOVE_SUBTREE_ARG_BASE_DN_FILE_DESCRIPTION.get(), true, true, true, false); baseDNFile.addLongIdentifier("entryDNFile"); parser.addArgument(baseDNFile); sizeLimit = new IntegerArgument('z', "sizeLimit", false, 1, INFO_MOVE_SUBTREE_ARG_SIZE_LIMIT_PLACEHOLDER.get(), INFO_MOVE_SUBTREE_ARG_SIZE_LIMIT_DESCRIPTION.get(), 0, Integer.MAX_VALUE, 0); parser.addArgument(sizeLimit); purpose = new StringArgument(null, "purpose", false, 1, INFO_MOVE_SUBTREE_ARG_PURPOSE_PLACEHOLDER.get(), INFO_MOVE_SUBTREE_ARG_PURPOSE_DESCRIPTION.get()); parser.addArgument(purpose); verbose = new BooleanArgument('v', "verbose", 1, INFO_MOVE_SUBTREE_ARG_VERBOSE_DESCRIPTION.get()); parser.addArgument(verbose); parser.addRequiredArgumentSet(baseDN, baseDNFile); parser.addExclusiveArgumentSet(baseDN, baseDNFile); } /** * {@inheritDoc} */ @Override() public LDAPConnectionOptions getConnectionOptions() { final LDAPConnectionOptions options = new LDAPConnectionOptions(); options.setUnsolicitedNotificationHandler(this); return options; } /** * Indicates whether this tool should provide arguments for redirecting output * to a file. If this method returns {@code true}, then the tool will offer * an "--outputFile" argument that will specify the path to a file to which * all standard output and standard error content will be written, and it will * also offer a "--teeToStandardOut" argument that can only be used if the * "--outputFile" argument is present and will cause all output to be written * to both the specified output file and to standard output. * * @return {@code true} if this tool should provide arguments for redirecting * output to a file, or {@code false} if not. */ @Override() protected boolean supportsOutputFile() { return true; } /** * Indicates whether this tool supports the use of a properties file for * specifying default values for arguments that aren't specified on the * command line. * * @return {@code true} if this tool supports the use of a properties file * for specifying default values for arguments that aren't specified * on the command line, or {@code false} if not. */ @Override() public boolean supportsPropertiesFile() { return true; } /** * {@inheritDoc} */ @Override() public ResultCode doToolProcessing() { final List baseDNs; if (baseDN.isPresent()) { final List dnList = baseDN.getValues(); baseDNs = new ArrayList(dnList.size()); for (final DN dn : dnList) { baseDNs.add(dn.toString()); } } else { try { baseDNs = baseDNFile.getNonBlankFileLines(); } catch (final Exception e) { Debug.debugException(e); err(ERR_MOVE_SUBTREE_ERROR_READING_BASE_DN_FILE.get( baseDNFile.getValue().getAbsolutePath(), StaticUtils.getExceptionMessage(e))); return ResultCode.LOCAL_ERROR; } if (baseDNs.isEmpty()) { err(ERR_MOVE_SUBTREE_BASE_DN_FILE_EMPTY.get( baseDNFile.getValue().getAbsolutePath())); return ResultCode.PARAM_ERROR; } } LDAPConnection sourceConnection = null; LDAPConnection targetConnection = null; try { try { sourceConnection = getConnection(0); } catch (LDAPException le) { Debug.debugException(le); err(ERR_MOVE_SUBTREE_CANNOT_CONNECT_TO_SOURCE.get( StaticUtils.getExceptionMessage(le))); return le.getResultCode(); } try { targetConnection = getConnection(1); } catch (LDAPException le) { Debug.debugException(le); err(ERR_MOVE_SUBTREE_CANNOT_CONNECT_TO_TARGET.get( StaticUtils.getExceptionMessage(le))); return le.getResultCode(); } sourceConnection.setConnectionName( INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get()); targetConnection.setConnectionName( INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get()); // We don't want to accidentally run with the same source and target // servers, so perform a couple of checks to verify that isn't the case. // First, perform a cheap check to rule out using the same address and // port for both source and target servers. if (sourceConnection.getConnectedAddress().equals( targetConnection.getConnectedAddress()) && (sourceConnection.getConnectedPort() == targetConnection.getConnectedPort())) { err(ERR_MOVE_SUBTREE_SAME_SOURCE_AND_TARGET_SERVERS.get()); return ResultCode.PARAM_ERROR; } // Next, retrieve the root DSE over each connection. Use it to verify // that both the startupUUID values are different as a check to ensure // that the source and target servers are different (this will be a // best-effort attempt, so if either startupUUID can't be retrieved, then // assume they're different servers). Also check to see whether the // source server supports the suppress referential integrity updates // control. boolean suppressReferentialIntegrityUpdates = false; try { final RootDSE sourceRootDSE = sourceConnection.getRootDSE(); final RootDSE targetRootDSE = targetConnection.getRootDSE(); if ((sourceRootDSE != null) && (targetRootDSE != null)) { final String sourceStartupUUID = sourceRootDSE.getAttributeValue(ATTR_STARTUP_UUID); final String targetStartupUUID = targetRootDSE.getAttributeValue(ATTR_STARTUP_UUID); if ((sourceStartupUUID != null) && sourceStartupUUID.equals(targetStartupUUID)) { err(ERR_MOVE_SUBTREE_SAME_SOURCE_AND_TARGET_SERVERS.get()); return ResultCode.PARAM_ERROR; } } if (sourceRootDSE != null) { suppressReferentialIntegrityUpdates = sourceRootDSE.supportsControl( SuppressReferentialIntegrityUpdatesRequestControl. SUPPRESS_REFINT_REQUEST_OID); } } catch (final Exception e) { Debug.debugException(e); } boolean first = true; ResultCode resultCode = ResultCode.SUCCESS; for (final String dn : baseDNs) { if (first) { first = false; } else { out(); } final OperationPurposeRequestControl operationPurpose; if (purpose.isPresent()) { operationPurpose = new OperationPurposeRequestControl( getToolName(), getToolVersion(), 20, purpose.getValue()); } else { operationPurpose = null; } final MoveSubtreeResult result = moveSubtreeWithRestrictedAccessibility( this, sourceConnection, targetConnection, dn, sizeLimit.getValue(), operationPurpose, suppressReferentialIntegrityUpdates, (verbose.isPresent() ? this : null)); if (result.getResultCode() == ResultCode.SUCCESS) { wrapOut(0, 79, INFO_MOVE_SUBTREE_RESULT_SUCCESSFUL.get( result.getEntriesAddedToTarget(), dn)); } else { if (resultCode == ResultCode.SUCCESS) { resultCode = result.getResultCode(); } wrapErr(0, 79, ERR_MOVE_SUBTREE_RESULT_UNSUCCESSFUL.get()); if (result.getErrorMessage() != null) { wrapErr(0, 79, ERR_MOVE_SUBTREE_ERROR_MESSAGE.get(result.getErrorMessage())); } if (result.getAdminActionRequired() != null) { wrapErr(0, 79, ERR_MOVE_SUBTREE_ADMIN_ACTION.get( result.getAdminActionRequired())); } } } return resultCode; } finally { if (sourceConnection!= null) { sourceConnection.close(); } if (targetConnection!= null) { targetConnection.close(); } } } /** * Moves a single leaf entry using a pair of interactive transactions. The * logic used to accomplish this is as follows: *
    *
  1. Start an interactive transaction in the source server.
  2. *
  3. Start an interactive transaction in the target server.
  4. *
  5. Read the entry from the source server. The search request will have * a subtree scope with a size limit of one, a filter of * "(objectClass=*)", will request all user and operational attributes, * and will include the following request controls: interactive * transaction specification, ManageDsaIT, LDAP subentries, return * conflict entries, soft-deleted entry access, real attributes only, * and operation purpose.
  6. *
  7. Add the entry to the target server. The add request will include the * following controls: interactive transaction specification, ignore * NO-USER-MODIFICATION, and operation purpose.
  8. *
  9. Delete the entry from the source server. The delete request will * include the following controls: interactive transaction * specification, ManageDsaIT, and operation purpose.
  10. *
  11. Commit the interactive transaction in the target server.
  12. *
  13. Commit the interactive transaction in the source server.
  14. *
* Conditions which could result in an incomplete move include: *
    *
  • The commit in the target server succeeds but the commit in the * source server fails. In this case, the entry may end up in both * servers, requiring manual cleanup. If this occurs, then the result * returned from this method will indicate this condition.
  • *
  • The account used to read entries from the source server does not have * permission to see all attributes in all entries. In this case, the * target server will include only a partial representation of the entry * in the source server. To avoid this problem, ensure that the account * used to read from the source server has sufficient access rights to * see all attributes in the entry to move.
  • *
  • The source server participates in replication and a change occurs to * the entry in a different server in the replicated environment while * the move is in progress. In this case, those changes may not be * reflected in the target server. To avoid this problem, it is * strongly recommended that all write access in the replication * environment containing the source server be directed to the source * server during the time that the move is in progress (e.g., using a * failover load-balancing algorithm in the Directory Proxy * Server).
  • *
* * @param sourceConnection A connection established to the source server. * It should be authenticated as a user with * permission to perform all of the operations * against the source server as referenced above. * @param targetConnection A connection established to the target server. * It should be authenticated as a user with * permission to perform all of the operations * against the target server as referenced above. * @param entryDN The base DN for the subtree to move. * @param opPurposeControl An optional operation purpose request control * that may be included in all requests sent to the * source and target servers. * @param listener An optional listener that may be invoked during * the course of moving entries from the source * server to the target server. * * @return An object with information about the result of the attempted * subtree move. */ public static MoveSubtreeResult moveEntryWithInteractiveTransaction( final LDAPConnection sourceConnection, final LDAPConnection targetConnection, final String entryDN, final OperationPurposeRequestControl opPurposeControl, final MoveSubtreeListener listener) { return moveEntryWithInteractiveTransaction(sourceConnection, targetConnection, entryDN, opPurposeControl, false, listener); } /** * Moves a single leaf entry using a pair of interactive transactions. The * logic used to accomplish this is as follows: *
    *
  1. Start an interactive transaction in the source server.
  2. *
  3. Start an interactive transaction in the target server.
  4. *
  5. Read the entry from the source server. The search request will have * a subtree scope with a size limit of one, a filter of * "(objectClass=*)", will request all user and operational attributes, * and will include the following request controls: interactive * transaction specification, ManageDsaIT, LDAP subentries, return * conflict entries, soft-deleted entry access, real attributes only, * and operation purpose.
  6. *
  7. Add the entry to the target server. The add request will include the * following controls: interactive transaction specification, ignore * NO-USER-MODIFICATION, and operation purpose.
  8. *
  9. Delete the entry from the source server. The delete request will * include the following controls: interactive transaction * specification, ManageDsaIT, and operation purpose.
  10. *
  11. Commit the interactive transaction in the target server.
  12. *
  13. Commit the interactive transaction in the source server.
  14. *
* Conditions which could result in an incomplete move include: *
    *
  • The commit in the target server succeeds but the commit in the * source server fails. In this case, the entry may end up in both * servers, requiring manual cleanup. If this occurs, then the result * returned from this method will indicate this condition.
  • *
  • The account used to read entries from the source server does not have * permission to see all attributes in all entries. In this case, the * target server will include only a partial representation of the entry * in the source server. To avoid this problem, ensure that the account * used to read from the source server has sufficient access rights to * see all attributes in the entry to move.
  • *
  • The source server participates in replication and a change occurs to * the entry in a different server in the replicated environment while * the move is in progress. In this case, those changes may not be * reflected in the target server. To avoid this problem, it is * strongly recommended that all write access in the replication * environment containing the source server be directed to the source * server during the time that the move is in progress (e.g., using a * failover load-balancing algorithm in the Directory Proxy * Server).
  • *
* * @param sourceConnection A connection established to the source server. * It should be authenticated as a user with * permission to perform all of the operations * against the source server as referenced above. * @param targetConnection A connection established to the target server. * It should be authenticated as a user with * permission to perform all of the operations * against the target server as referenced above. * @param entryDN The base DN for the subtree to move. * @param opPurposeControl An optional operation purpose request control * that may be included in all requests sent to the * source and target servers. * @param suppressRefInt Indicates whether to include a request control * causing referential integrity updates to be * suppressed on the source server. * @param listener An optional listener that may be invoked during * the course of moving entries from the source * server to the target server. * * @return An object with information about the result of the attempted * subtree move. */ public static MoveSubtreeResult moveEntryWithInteractiveTransaction( final LDAPConnection sourceConnection, final LDAPConnection targetConnection, final String entryDN, final OperationPurposeRequestControl opPurposeControl, final boolean suppressRefInt, final MoveSubtreeListener listener) { final StringBuilder errorMsg = new StringBuilder(); final StringBuilder adminMsg = new StringBuilder(); final ReverseComparator reverseComparator = new ReverseComparator(); final TreeSet sourceEntryDNs = new TreeSet(reverseComparator); final AtomicInteger entriesReadFromSource = new AtomicInteger(0); final AtomicInteger entriesAddedToTarget = new AtomicInteger(0); final AtomicInteger entriesDeletedFromSource = new AtomicInteger(0); final AtomicReference resultCode = new AtomicReference(); ASN1OctetString sourceTxnID = null; ASN1OctetString targetTxnID = null; boolean sourceServerAltered = false; boolean targetServerAltered = false; processingBlock: try { // Start an interactive transaction in the source server. final InteractiveTransactionSpecificationRequestControl sourceTxnControl; try { final StartInteractiveTransactionExtendedRequest startTxnRequest; if (opPurposeControl == null) { startTxnRequest = new StartInteractiveTransactionExtendedRequest(entryDN); } else { startTxnRequest = new StartInteractiveTransactionExtendedRequest( entryDN, new Control[]{opPurposeControl}); } final StartInteractiveTransactionExtendedResult startTxnResult = (StartInteractiveTransactionExtendedResult) sourceConnection.processExtendedOperation(startTxnRequest); if (startTxnResult.getResultCode() == ResultCode.SUCCESS) { sourceTxnID = startTxnResult.getTransactionID(); sourceTxnControl = new InteractiveTransactionSpecificationRequestControl( sourceTxnID, true, true); } else { resultCode.compareAndSet(null, startTxnResult.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_START_SOURCE_TXN.get( startTxnResult.getDiagnosticMessage()), errorMsg); break processingBlock; } } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_START_SOURCE_TXN.get( StaticUtils.getExceptionMessage(le)), errorMsg); break processingBlock; } // Start an interactive transaction in the target server. final InteractiveTransactionSpecificationRequestControl targetTxnControl; try { final StartInteractiveTransactionExtendedRequest startTxnRequest; if (opPurposeControl == null) { startTxnRequest = new StartInteractiveTransactionExtendedRequest(entryDN); } else { startTxnRequest = new StartInteractiveTransactionExtendedRequest( entryDN, new Control[]{opPurposeControl}); } final StartInteractiveTransactionExtendedResult startTxnResult = (StartInteractiveTransactionExtendedResult) targetConnection.processExtendedOperation(startTxnRequest); if (startTxnResult.getResultCode() == ResultCode.SUCCESS) { targetTxnID = startTxnResult.getTransactionID(); targetTxnControl = new InteractiveTransactionSpecificationRequestControl( targetTxnID, true, true); } else { resultCode.compareAndSet(null, startTxnResult.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_START_TARGET_TXN.get( startTxnResult.getDiagnosticMessage()), errorMsg); break processingBlock; } } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_START_TARGET_TXN.get( StaticUtils.getExceptionMessage(le)), errorMsg); break processingBlock; } // Perform a search to find all entries in the target subtree, and include // a search listener that will add each entry to the target server as it // is returned from the source server. final Control[] searchControls; if (opPurposeControl == null) { searchControls = new Control[] { sourceTxnControl, new ManageDsaITRequestControl(true), new SubentriesRequestControl(true), new ReturnConflictEntriesRequestControl(true), new SoftDeletedEntryAccessRequestControl(true, true, false), new RealAttributesOnlyRequestControl(true) }; } else { searchControls = new Control[] { sourceTxnControl, new ManageDsaITRequestControl(true), new SubentriesRequestControl(true), new ReturnConflictEntriesRequestControl(true), new SoftDeletedEntryAccessRequestControl(true, true, false), new RealAttributesOnlyRequestControl(true), opPurposeControl }; } final MoveSubtreeTxnSearchListener searchListener = new MoveSubtreeTxnSearchListener(targetConnection, resultCode, errorMsg, entriesReadFromSource, entriesAddedToTarget, sourceEntryDNs, targetTxnControl, opPurposeControl, listener); final SearchRequest searchRequest = new SearchRequest( searchListener, searchControls, entryDN, SearchScope.SUB, DereferencePolicy.NEVER, 1, 0, false, Filter.createPresenceFilter("objectClass"), "*", "+"); SearchResult searchResult; try { searchResult = sourceConnection.search(searchRequest); } catch (final LDAPSearchException lse) { Debug.debugException(lse); searchResult = lse.getSearchResult(); } if (searchResult.getResultCode() == ResultCode.SUCCESS) { try { final InteractiveTransactionSpecificationResponseControl txnResult = InteractiveTransactionSpecificationResponseControl.get( searchResult); if ((txnResult == null) || (! txnResult.transactionValid())) { resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); append(ERR_MOVE_ENTRY_SEARCH_TXN_NO_LONGER_VALID.get(), errorMsg); break processingBlock; } } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_DECODE_SEARCH_TXN_CONTROL.get( StaticUtils.getExceptionMessage(le)), errorMsg); break processingBlock; } } else { resultCode.compareAndSet(null, searchResult.getResultCode()); append( ERR_MOVE_SUBTREE_SEARCH_FAILED.get(entryDN, searchResult.getDiagnosticMessage()), errorMsg); try { final InteractiveTransactionSpecificationResponseControl txnResult = InteractiveTransactionSpecificationResponseControl.get( searchResult); if ((txnResult != null) && (! txnResult.transactionValid())) { sourceTxnID = null; } } catch (final LDAPException le) { Debug.debugException(le); } if (! searchListener.targetTransactionValid()) { targetTxnID = null; } break processingBlock; } // If an error occurred during add processing, then fail. if (resultCode.get() == null) { targetServerAltered = true; } else { break processingBlock; } // Delete each of the entries in the source server. The map should // already be sorted in reverse order (as a result of the comparator used // when creating it), so it will guarantee children are deleted before // their parents. final ArrayList deleteControlList = new ArrayList(4); deleteControlList.add(sourceTxnControl); deleteControlList.add(new ManageDsaITRequestControl(true)); if (opPurposeControl != null) { deleteControlList.add(opPurposeControl); } if (suppressRefInt) { deleteControlList.add( new SuppressReferentialIntegrityUpdatesRequestControl(false)); } final Control[] deleteControls = new Control[deleteControlList.size()]; deleteControlList.toArray(deleteControls); for (final DN dn : sourceEntryDNs) { if (listener != null) { try { listener.doPreDeleteProcessing(dn); } catch (final Exception e) { Debug.debugException(e); resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); append( ERR_MOVE_SUBTREE_PRE_DELETE_FAILURE.get(dn.toString(), StaticUtils.getExceptionMessage(e)), errorMsg); break processingBlock; } } LDAPResult deleteResult; try { deleteResult = sourceConnection.delete( new DeleteRequest(dn, deleteControls)); } catch (final LDAPException le) { Debug.debugException(le); deleteResult = le.toLDAPResult(); } if (deleteResult.getResultCode() == ResultCode.SUCCESS) { sourceServerAltered = true; entriesDeletedFromSource.incrementAndGet(); try { final InteractiveTransactionSpecificationResponseControl txnResult = InteractiveTransactionSpecificationResponseControl.get( deleteResult); if ((txnResult == null) || (! txnResult.transactionValid())) { resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); append( ERR_MOVE_ENTRY_DELETE_TXN_NO_LONGER_VALID.get( dn.toString()), errorMsg); break processingBlock; } } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_DECODE_DELETE_TXN_CONTROL.get( dn.toString(), StaticUtils.getExceptionMessage(le)), errorMsg); break processingBlock; } } else { resultCode.compareAndSet(null, deleteResult.getResultCode()); append( ERR_MOVE_SUBTREE_DELETE_FAILURE.get( dn.toString(), deleteResult.getDiagnosticMessage()), errorMsg); try { final InteractiveTransactionSpecificationResponseControl txnResult = InteractiveTransactionSpecificationResponseControl.get( deleteResult); if ((txnResult != null) && (! txnResult.transactionValid())) { sourceTxnID = null; } } catch (final LDAPException le) { Debug.debugException(le); } break processingBlock; } if (listener != null) { try { listener.doPostDeleteProcessing(dn); } catch (final Exception e) { Debug.debugException(e); resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); append( ERR_MOVE_SUBTREE_POST_DELETE_FAILURE.get(dn.toString(), StaticUtils.getExceptionMessage(e)), errorMsg); break processingBlock; } } } // Commit the transaction in the target server. try { final EndInteractiveTransactionExtendedRequest commitRequest; if (opPurposeControl == null) { commitRequest = new EndInteractiveTransactionExtendedRequest( targetTxnID, true); } else { commitRequest = new EndInteractiveTransactionExtendedRequest( targetTxnID, true, new Control[] { opPurposeControl }); } final ExtendedResult commitResult = targetConnection.processExtendedOperation(commitRequest); if (commitResult.getResultCode() == ResultCode.SUCCESS) { targetTxnID = null; } else { resultCode.compareAndSet(null, commitResult.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_COMMIT_TARGET_TXN.get( commitResult.getDiagnosticMessage()), errorMsg); break processingBlock; } } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_COMMIT_TARGET_TXN.get( StaticUtils.getExceptionMessage(le)), errorMsg); break processingBlock; } // Commit the transaction in the source server. try { final EndInteractiveTransactionExtendedRequest commitRequest; if (opPurposeControl == null) { commitRequest = new EndInteractiveTransactionExtendedRequest( sourceTxnID, true); } else { commitRequest = new EndInteractiveTransactionExtendedRequest( sourceTxnID, true, new Control[] { opPurposeControl }); } final ExtendedResult commitResult = sourceConnection.processExtendedOperation(commitRequest); if (commitResult.getResultCode() == ResultCode.SUCCESS) { sourceTxnID = null; } else { resultCode.compareAndSet(null, commitResult.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_COMMIT_SOURCE_TXN.get( commitResult.getDiagnosticMessage()), errorMsg); break processingBlock; } } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append( ERR_MOVE_ENTRY_CANNOT_COMMIT_SOURCE_TXN.get( StaticUtils.getExceptionMessage(le)), errorMsg); append(ERR_MOVE_ENTRY_EXISTS_IN_BOTH_SERVERS.get(entryDN), adminMsg); break processingBlock; } } finally { // If the transaction is still active in the target server, then abort it. if (targetTxnID != null) { try { final EndInteractiveTransactionExtendedRequest abortRequest; if (opPurposeControl == null) { abortRequest = new EndInteractiveTransactionExtendedRequest( targetTxnID, false); } else { abortRequest = new EndInteractiveTransactionExtendedRequest( targetTxnID, false, new Control[] { opPurposeControl }); } final ExtendedResult abortResult = targetConnection.processExtendedOperation(abortRequest); if (abortResult.getResultCode() == ResultCode.INTERACTIVE_TRANSACTION_ABORTED) { targetServerAltered = false; entriesAddedToTarget.set(0); append(INFO_MOVE_ENTRY_TARGET_ABORT_SUCCEEDED.get(), errorMsg); } else { append( ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE.get( abortResult.getDiagnosticMessage()), errorMsg); append( ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE_ADMIN_ACTTION.get( entryDN), adminMsg); } } catch (final Exception e) { Debug.debugException(e); append( ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE.get( StaticUtils.getExceptionMessage(e)), errorMsg); append( ERR_MOVE_ENTRY_TARGET_ABORT_FAILURE_ADMIN_ACTTION.get( entryDN), adminMsg); } } // If the transaction is still active in the source server, then abort it. if (sourceTxnID != null) { try { final EndInteractiveTransactionExtendedRequest abortRequest; if (opPurposeControl == null) { abortRequest = new EndInteractiveTransactionExtendedRequest( sourceTxnID, false); } else { abortRequest = new EndInteractiveTransactionExtendedRequest( sourceTxnID, false, new Control[] { opPurposeControl }); } final ExtendedResult abortResult = sourceConnection.processExtendedOperation(abortRequest); if (abortResult.getResultCode() == ResultCode.INTERACTIVE_TRANSACTION_ABORTED) { sourceServerAltered = false; entriesDeletedFromSource.set(0); append(INFO_MOVE_ENTRY_SOURCE_ABORT_SUCCEEDED.get(), errorMsg); } else { append( ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE.get( abortResult.getDiagnosticMessage()), errorMsg); append( ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE_ADMIN_ACTTION.get( entryDN), adminMsg); } } catch (final Exception e) { Debug.debugException(e); append( ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE.get( StaticUtils.getExceptionMessage(e)), errorMsg); append( ERR_MOVE_ENTRY_SOURCE_ABORT_FAILURE_ADMIN_ACTTION.get( entryDN), adminMsg); } } } // Construct the result to return to the client. resultCode.compareAndSet(null, ResultCode.SUCCESS); final String errorMessage; if (errorMsg.length() > 0) { errorMessage = errorMsg.toString(); } else { errorMessage = null; } final String adminActionRequired; if (adminMsg.length() > 0) { adminActionRequired = adminMsg.toString(); } else { adminActionRequired = null; } return new MoveSubtreeResult(resultCode.get(), errorMessage, adminActionRequired, sourceServerAltered, targetServerAltered, entriesReadFromSource.get(), entriesAddedToTarget.get(), entriesDeletedFromSource.get()); } /** * Moves a subtree of entries using a process in which access to the subtree * will be restricted while the move is in progress. While entries are being * read from the source server and added to the target server, the subtree * will be read-only in the source server and hidden in the target server. * While entries are being removed from the source server, the subtree will be * hidden in the source server while fully accessible in the target. After * all entries have been removed from the source server, the accessibility * restriction will be removed from that server as well. *

* The logic used to accomplish this is as follows: *
    *
  1. Make the subtree hidden in the target server.
  2. *
  3. Make the subtree read-only in the source server.
  4. *
  5. Perform a search in the source server to retrieve all entries in the * specified subtree. The search request will have a subtree scope with * a filter of "(objectClass=*)", will include the specified size limit, * will request all user and operational attributes, and will include * the following request controls: ManageDsaIT, LDAP subentries, * return conflict entries, soft-deleted entry access, real attributes * only, and operation purpose.
  6. *
  7. For each entry returned by the search, add that entry to the target * server. This method assumes that the source server will return * results in a manner that guarantees that no child entry is returned * before its parent. Each add request will include the following * controls: ignore NO-USER-MODIFICATION, and operation purpose.
  8. *
  9. Make the subtree read-only in the target server.
  10. *
  11. Make the subtree hidden in the source server.
  12. *
  13. Make the subtree accessible in the target server.
  14. *
  15. Delete each entry from the source server, with all subordinate entries * before their parents. Each delete request will include the following * controls: ManageDsaIT, and operation purpose.
  16. *
  17. Make the subtree accessible in the source server.
  18. *
* Conditions which could result in an incomplete move include: *
    *
  • A failure is encountered while altering the accessibility of the * subtree in either the source or target server.
  • *
  • A failure is encountered while attempting to process an add in the * target server and a subsequent failure is encountered when attempting * to delete previously-added entries.
  • *
  • A failure is encountered while attempting to delete one or more * entries from the source server.
  • *
* * @param sourceConnection A connection established to the source server. * It should be authenticated as a user with * permission to perform all of the operations * against the source server as referenced above. * @param targetConnection A connection established to the target server. * It should be authenticated as a user with * permission to perform all of the operations * against the target server as referenced above. * @param baseDN The base DN for the subtree to move. * @param sizeLimit The maximum number of entries to be moved. It * may be less than or equal to zero to indicate * that no client-side limit should be enforced * (although the server may still enforce its own * limit). * @param opPurposeControl An optional operation purpose request control * that may be included in all requests sent to the * source and target servers. * @param listener An optional listener that may be invoked during * the course of moving entries from the source * server to the target server. * * @return An object with information about the result of the attempted * subtree move. */ public static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility( final LDAPConnection sourceConnection, final LDAPConnection targetConnection, final String baseDN, final int sizeLimit, final OperationPurposeRequestControl opPurposeControl, final MoveSubtreeListener listener) { return moveSubtreeWithRestrictedAccessibility(sourceConnection, targetConnection, baseDN, sizeLimit, opPurposeControl, false, listener); } /** * Moves a subtree of entries using a process in which access to the subtree * will be restricted while the move is in progress. While entries are being * read from the source server and added to the target server, the subtree * will be read-only in the source server and hidden in the target server. * While entries are being removed from the source server, the subtree will be * hidden in the source server while fully accessible in the target. After * all entries have been removed from the source server, the accessibility * restriction will be removed from that server as well. *

* The logic used to accomplish this is as follows: *
    *
  1. Make the subtree hidden in the target server.
  2. *
  3. Make the subtree read-only in the source server.
  4. *
  5. Perform a search in the source server to retrieve all entries in the * specified subtree. The search request will have a subtree scope with * a filter of "(objectClass=*)", will include the specified size limit, * will request all user and operational attributes, and will include * the following request controls: ManageDsaIT, LDAP subentries, * return conflict entries, soft-deleted entry access, real attributes * only, and operation purpose.
  6. *
  7. For each entry returned by the search, add that entry to the target * server. This method assumes that the source server will return * results in a manner that guarantees that no child entry is returned * before its parent. Each add request will include the following * controls: ignore NO-USER-MODIFICATION, and operation purpose.
  8. *
  9. Make the subtree read-only in the target server.
  10. *
  11. Make the subtree hidden in the source server.
  12. *
  13. Make the subtree accessible in the target server.
  14. *
  15. Delete each entry from the source server, with all subordinate entries * before their parents. Each delete request will include the following * controls: ManageDsaIT, and operation purpose.
  16. *
  17. Make the subtree accessible in the source server.
  18. *
* Conditions which could result in an incomplete move include: *
    *
  • A failure is encountered while altering the accessibility of the * subtree in either the source or target server.
  • *
  • A failure is encountered while attempting to process an add in the * target server and a subsequent failure is encountered when attempting * to delete previously-added entries.
  • *
  • A failure is encountered while attempting to delete one or more * entries from the source server.
  • *
* * @param sourceConnection A connection established to the source server. * It should be authenticated as a user with * permission to perform all of the operations * against the source server as referenced above. * @param targetConnection A connection established to the target server. * It should be authenticated as a user with * permission to perform all of the operations * against the target server as referenced above. * @param baseDN The base DN for the subtree to move. * @param sizeLimit The maximum number of entries to be moved. It * may be less than or equal to zero to indicate * that no client-side limit should be enforced * (although the server may still enforce its own * limit). * @param opPurposeControl An optional operation purpose request control * that may be included in all requests sent to the * source and target servers. * @param suppressRefInt Indicates whether to include a request control * causing referential integrity updates to be * suppressed on the source server. * @param listener An optional listener that may be invoked during * the course of moving entries from the source * server to the target server. * * @return An object with information about the result of the attempted * subtree move. */ public static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility( final LDAPConnection sourceConnection, final LDAPConnection targetConnection, final String baseDN, final int sizeLimit, final OperationPurposeRequestControl opPurposeControl, final boolean suppressRefInt, final MoveSubtreeListener listener) { return moveSubtreeWithRestrictedAccessibility(null, sourceConnection, targetConnection, baseDN, sizeLimit, opPurposeControl, suppressRefInt, listener); } /** * Performs the real {@code moveSubtreeWithRestrictedAccessibility} * processing. If a tool is available, this method will update state * information in that tool so that it can be referenced by a shutdown hook * in the event that processing is interrupted. * * @param tool A reference to a tool instance to be updated with * state information. * @param sourceConnection A connection established to the source server. * It should be authenticated as a user with * permission to perform all of the operations * against the source server as referenced above. * @param targetConnection A connection established to the target server. * It should be authenticated as a user with * permission to perform all of the operations * against the target server as referenced above. * @param baseDN The base DN for the subtree to move. * @param sizeLimit The maximum number of entries to be moved. It * may be less than or equal to zero to indicate * that no client-side limit should be enforced * (although the server may still enforce its own * limit). * @param opPurposeControl An optional operation purpose request control * that may be included in all requests sent to the * source and target servers. * @param suppressRefInt Indicates whether to include a request control * causing referential integrity updates to be * suppressed on the source server. * @param listener An optional listener that may be invoked during * the course of moving entries from the source * server to the target server. * * @return An object with information about the result of the attempted * subtree move. */ private static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility( final MoveSubtree tool, final LDAPConnection sourceConnection, final LDAPConnection targetConnection, final String baseDN, final int sizeLimit, final OperationPurposeRequestControl opPurposeControl, final boolean suppressRefInt, final MoveSubtreeListener listener) { // Ensure that the subtree is currently accessible in both the source and // target servers. final MoveSubtreeResult initialAccessibilityResult = checkInitialAccessibility(sourceConnection, targetConnection, baseDN, opPurposeControl); if (initialAccessibilityResult != null) { return initialAccessibilityResult; } final StringBuilder errorMsg = new StringBuilder(); final StringBuilder adminMsg = new StringBuilder(); final ReverseComparator reverseComparator = new ReverseComparator(); final TreeSet sourceEntryDNs = new TreeSet(reverseComparator); final AtomicInteger entriesReadFromSource = new AtomicInteger(0); final AtomicInteger entriesAddedToTarget = new AtomicInteger(0); final AtomicInteger entriesDeletedFromSource = new AtomicInteger(0); final AtomicReference resultCode = new AtomicReference(); boolean sourceServerAltered = false; boolean targetServerAltered = false; SubtreeAccessibilityState currentSourceState = SubtreeAccessibilityState.ACCESSIBLE; SubtreeAccessibilityState currentTargetState = SubtreeAccessibilityState.ACCESSIBLE; processingBlock: { // Identify the users authenticated on each connection. final String sourceUserDN; final String targetUserDN; try { sourceUserDN = getAuthenticatedUserDN(sourceConnection, true, opPurposeControl); targetUserDN = getAuthenticatedUserDN(targetConnection, false, opPurposeControl); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } // Make the subtree hidden on the target server. try { setAccessibility(targetConnection, false, baseDN, SubtreeAccessibilityState.HIDDEN, targetUserDN, opPurposeControl); currentTargetState = SubtreeAccessibilityState.HIDDEN; setInterruptMessage(tool, WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_HIDDEN.get(baseDN, targetConnection.getConnectedAddress(), targetConnection.getConnectedPort())); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } // Make the subtree read-only on the source server. try { setAccessibility(sourceConnection, true, baseDN, SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, sourceUserDN, opPurposeControl); currentSourceState = SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED; setInterruptMessage(tool, WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_READ_ONLY.get(baseDN, targetConnection.getConnectedAddress(), targetConnection.getConnectedPort(), sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort())); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } // Perform a search to find all entries in the target subtree, and include // a search listener that will add each entry to the target server as it // is returned from the source server. final Control[] searchControls; if (opPurposeControl == null) { searchControls = new Control[] { new ManageDsaITRequestControl(true), new SubentriesRequestControl(true), new ReturnConflictEntriesRequestControl(true), new SoftDeletedEntryAccessRequestControl(true, true, false), new RealAttributesOnlyRequestControl(true) }; } else { searchControls = new Control[] { new ManageDsaITRequestControl(true), new SubentriesRequestControl(true), new ReturnConflictEntriesRequestControl(true), new SoftDeletedEntryAccessRequestControl(true, true, false), new RealAttributesOnlyRequestControl(true), opPurposeControl }; } final MoveSubtreeAccessibilitySearchListener searchListener = new MoveSubtreeAccessibilitySearchListener(tool, baseDN, sourceConnection, targetConnection, resultCode, errorMsg, entriesReadFromSource, entriesAddedToTarget, sourceEntryDNs, opPurposeControl, listener); final SearchRequest searchRequest = new SearchRequest( searchListener, searchControls, baseDN, SearchScope.SUB, DereferencePolicy.NEVER, sizeLimit, 0, false, Filter.createPresenceFilter("objectClass"), "*", "+"); SearchResult searchResult; try { searchResult = sourceConnection.search(searchRequest); } catch (final LDAPSearchException lse) { Debug.debugException(lse); searchResult = lse.getSearchResult(); } if (entriesAddedToTarget.get() > 0) { targetServerAltered = true; } if (searchResult.getResultCode() != ResultCode.SUCCESS) { resultCode.compareAndSet(null, searchResult.getResultCode()); append( ERR_MOVE_SUBTREE_SEARCH_FAILED.get(baseDN, searchResult.getDiagnosticMessage()), errorMsg); final AtomicInteger deleteCount = new AtomicInteger(0); if (targetServerAltered) { deleteEntries(targetConnection, false, sourceEntryDNs, opPurposeControl, false, null, deleteCount, resultCode, errorMsg); entriesAddedToTarget.addAndGet(0 - deleteCount.get()); if (entriesAddedToTarget.get() == 0) { targetServerAltered = false; } else { append(ERR_MOVE_SUBTREE_TARGET_NOT_DELETED_ADMIN_ACTION.get(baseDN), adminMsg); } } break processingBlock; } // If an error occurred during add processing, then fail. if (resultCode.get() != null) { final AtomicInteger deleteCount = new AtomicInteger(0); if (targetServerAltered) { deleteEntries(targetConnection, false, sourceEntryDNs, opPurposeControl, false, null, deleteCount, resultCode, errorMsg); entriesAddedToTarget.addAndGet(0 - deleteCount.get()); if (entriesAddedToTarget.get() == 0) { targetServerAltered = false; } else { append(ERR_MOVE_SUBTREE_TARGET_NOT_DELETED_ADMIN_ACTION.get(baseDN), adminMsg); } } break processingBlock; } // Make the subtree read-only on the target server. try { setAccessibility(targetConnection, true, baseDN, SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, targetUserDN, opPurposeControl); currentTargetState = SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED; setInterruptMessage(tool, WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_READ_ONLY.get(baseDN, sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort(), targetConnection.getConnectedAddress(), targetConnection.getConnectedPort())); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } // Make the subtree hidden on the source server. try { setAccessibility(sourceConnection, true, baseDN, SubtreeAccessibilityState.HIDDEN, sourceUserDN, opPurposeControl); currentSourceState = SubtreeAccessibilityState.HIDDEN; setInterruptMessage(tool, WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_HIDDEN.get(baseDN, sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort(), targetConnection.getConnectedAddress(), targetConnection.getConnectedPort())); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } // Make the subtree accessible on the target server. try { setAccessibility(targetConnection, true, baseDN, SubtreeAccessibilityState.ACCESSIBLE, targetUserDN, opPurposeControl); currentTargetState = SubtreeAccessibilityState.ACCESSIBLE; setInterruptMessage(tool, WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_ACCESSIBLE.get(baseDN, sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort(), targetConnection.getConnectedAddress(), targetConnection.getConnectedPort())); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } // Delete each of the entries in the source server. The map should // already be sorted in reverse order (as a result of the comparator used // when creating it), so it will guarantee children are deleted before // their parents. final boolean deleteSuccessful = deleteEntries(sourceConnection, true, sourceEntryDNs, opPurposeControl, suppressRefInt, listener, entriesDeletedFromSource, resultCode, errorMsg); sourceServerAltered = (entriesDeletedFromSource.get() != 0); if (! deleteSuccessful) { append(ERR_MOVE_SUBTREE_SOURCE_NOT_DELETED_ADMIN_ACTION.get(baseDN), adminMsg); break processingBlock; } // Make the subtree accessible on the source server. try { setAccessibility(sourceConnection, true, baseDN, SubtreeAccessibilityState.ACCESSIBLE, sourceUserDN, opPurposeControl); currentSourceState = SubtreeAccessibilityState.ACCESSIBLE; setInterruptMessage(tool, null); } catch (final LDAPException le) { Debug.debugException(le); resultCode.compareAndSet(null, le.getResultCode()); append(le.getMessage(), errorMsg); break processingBlock; } } // If the source server was left in a state other than accessible, then // see if we can safely change it back. If it's left in any state other // then accessible, then generate an admin action message. if (currentSourceState != SubtreeAccessibilityState.ACCESSIBLE) { if (! sourceServerAltered) { try { setAccessibility(sourceConnection, true, baseDN, SubtreeAccessibilityState.ACCESSIBLE, null, opPurposeControl); currentSourceState = SubtreeAccessibilityState.ACCESSIBLE; } catch (final LDAPException le) { Debug.debugException(le); } } if (currentSourceState != SubtreeAccessibilityState.ACCESSIBLE) { append( ERR_MOVE_SUBTREE_SOURCE_LEFT_INACCESSIBLE.get( currentSourceState, baseDN), adminMsg); } } // If the target server was left in a state other than accessible, then // see if we can safely change it back. If it's left in any state other // then accessible, then generate an admin action message. if (currentTargetState != SubtreeAccessibilityState.ACCESSIBLE) { if (! targetServerAltered) { try { setAccessibility(targetConnection, false, baseDN, SubtreeAccessibilityState.ACCESSIBLE, null, opPurposeControl); currentTargetState = SubtreeAccessibilityState.ACCESSIBLE; } catch (final LDAPException le) { Debug.debugException(le); } } if (currentTargetState != SubtreeAccessibilityState.ACCESSIBLE) { append( ERR_MOVE_SUBTREE_TARGET_LEFT_INACCESSIBLE.get( currentTargetState, baseDN), adminMsg); } } // Construct the result to return to the client. resultCode.compareAndSet(null, ResultCode.SUCCESS); final String errorMessage; if (errorMsg.length() > 0) { errorMessage = errorMsg.toString(); } else { errorMessage = null; } final String adminActionRequired; if (adminMsg.length() > 0) { adminActionRequired = adminMsg.toString(); } else { adminActionRequired = null; } return new MoveSubtreeResult(resultCode.get(), errorMessage, adminActionRequired, sourceServerAltered, targetServerAltered, entriesReadFromSource.get(), entriesAddedToTarget.get(), entriesDeletedFromSource.get()); } /** * Retrieves the DN of the user authenticated on the provided connection. It * will first try to look at the last successful bind request processed on the * connection, and will fall back to using the "Who Am I?" extended request. * * @param connection The connection for which to make the * determination. * @param isSource Indicates whether the connection is to the source * or target server. * @param opPurposeControl An optional operation purpose request control * that may be included in the request. * * @return The DN of the user authenticated on the provided connection, or * {@code null} if the connection is not authenticated. * * @throws LDAPException If a problem is encountered while making the * determination. */ private static String getAuthenticatedUserDN(final LDAPConnection connection, final boolean isSource, final OperationPurposeRequestControl opPurposeControl) throws LDAPException { final BindRequest bindRequest = InternalSDKHelper.getLastBindRequest(connection); if ((bindRequest != null) && (bindRequest instanceof SimpleBindRequest)) { final SimpleBindRequest r = (SimpleBindRequest) bindRequest; return r.getBindDN(); } final Control[] controls; if (opPurposeControl == null) { controls = StaticUtils.NO_CONTROLS; } else { controls = new Control[] { opPurposeControl }; } final String connectionName = isSource ? INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get() : INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(); final WhoAmIExtendedResult whoAmIResult; try { whoAmIResult = (WhoAmIExtendedResult) connection.processExtendedOperation( new WhoAmIExtendedRequest(controls)); } catch (final LDAPException le) { Debug.debugException(le); throw new LDAPException(le.getResultCode(), ERR_MOVE_SUBTREE_ERROR_INVOKING_WHO_AM_I.get(connectionName, StaticUtils.getExceptionMessage(le)), le); } if (whoAmIResult.getResultCode() != ResultCode.SUCCESS) { throw new LDAPException(whoAmIResult.getResultCode(), ERR_MOVE_SUBTREE_ERROR_INVOKING_WHO_AM_I.get(connectionName, whoAmIResult.getDiagnosticMessage())); } final String authzID = whoAmIResult.getAuthorizationID(); if ((authzID != null) && authzID.startsWith("dn:")) { return authzID.substring(3); } else { throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM, ERR_MOVE_SUBTREE_CANNOT_IDENTIFY_CONNECTED_USER.get(connectionName)); } } /** * Ensures that the specified subtree is accessible in both the source and * target servers. If it is not accessible, then it may indicate that another * administrative operation is in progress for the subtree, or that a previous * move-subtree operation was interrupted before it could complete. * * @param sourceConnection The connection to use to communicate with the * source directory server. * @param targetConnection The connection to use to communicate with the * target directory server. * @param baseDN The base DN for which to verify accessibility. * @param opPurposeControl An optional operation purpose request control * that may be included in the requests. * * @return {@code null} if the specified subtree is accessible in both the * source and target servers, or a non-{@code null} object with the * result that should be used if there is an accessibility problem * with the subtree on the source and/or target server. */ private static MoveSubtreeResult checkInitialAccessibility( final LDAPConnection sourceConnection, final LDAPConnection targetConnection, final String baseDN, final OperationPurposeRequestControl opPurposeControl) { final DN parsedBaseDN; try { parsedBaseDN = new DN(baseDN); } catch (final Exception e) { Debug.debugException(e); return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX, ERR_MOVE_SUBTREE_CANNOT_PARSE_BASE_DN.get(baseDN, StaticUtils.getExceptionMessage(e)), null, false, false, 0, 0, 0); } final Control[] controls; if (opPurposeControl == null) { controls = StaticUtils.NO_CONTROLS; } else { controls = new Control[] { opPurposeControl }; } // Get the restrictions from the source server. If there are any, then // make sure that nothing in the hierarchy of the base DN is non-accessible. final GetSubtreeAccessibilityExtendedResult sourceResult; try { sourceResult = (GetSubtreeAccessibilityExtendedResult) sourceConnection.processExtendedOperation( new GetSubtreeAccessibilityExtendedRequest(controls)); if (sourceResult.getResultCode() != ResultCode.SUCCESS) { throw new LDAPException(sourceResult); } } catch (final LDAPException le) { Debug.debugException(le); return new MoveSubtreeResult(le.getResultCode(), ERR_MOVE_SUBTREE_CANNOT_GET_ACCESSIBILITY_STATE.get(baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), le.getMessage()), null, false, false, 0, 0, 0); } boolean sourceMatch = false; String sourceMessage = null; SubtreeAccessibilityRestriction sourceRestriction = null; final List sourceRestrictions = sourceResult.getAccessibilityRestrictions(); if (sourceRestrictions != null) { for (final SubtreeAccessibilityRestriction r : sourceRestrictions) { if (r.getAccessibilityState() == SubtreeAccessibilityState.ACCESSIBLE) { continue; } final DN restrictionDN; try { restrictionDN = new DN(r.getSubtreeBaseDN()); } catch (final Exception e) { Debug.debugException(e); return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX, ERR_MOVE_SUBTREE_CANNOT_PARSE_RESTRICTION_BASE_DN.get( r.getSubtreeBaseDN(), INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), r.toString(), StaticUtils.getExceptionMessage(e)), null, false, false, 0, 0, 0); } if (restrictionDN.equals(parsedBaseDN)) { sourceMatch = true; sourceRestriction = r; sourceMessage = ERR_MOVE_SUBTREE_NOT_ACCESSIBLE.get(baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), r.getAccessibilityState().getStateName()); break; } else if (restrictionDN.isAncestorOf(parsedBaseDN, false)) { sourceRestriction = r; sourceMessage = ERR_MOVE_SUBTREE_WITHIN_UNACCESSIBLE_TREE.get(baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); break; } else if (restrictionDN.isDescendantOf(parsedBaseDN, false)) { sourceRestriction = r; sourceMessage = ERR_MOVE_SUBTREE_CONTAINS_UNACCESSIBLE_TREE.get( baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(), r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); break; } } } // Get the restrictions from the target server. If there are any, then // make sure that nothing in the hierarchy of the base DN is non-accessible. final GetSubtreeAccessibilityExtendedResult targetResult; try { targetResult = (GetSubtreeAccessibilityExtendedResult) targetConnection.processExtendedOperation( new GetSubtreeAccessibilityExtendedRequest(controls)); if (targetResult.getResultCode() != ResultCode.SUCCESS) { throw new LDAPException(targetResult); } } catch (final LDAPException le) { Debug.debugException(le); return new MoveSubtreeResult(le.getResultCode(), ERR_MOVE_SUBTREE_CANNOT_GET_ACCESSIBILITY_STATE.get(baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), le.getMessage()), null, false, false, 0, 0, 0); } boolean targetMatch = false; String targetMessage = null; SubtreeAccessibilityRestriction targetRestriction = null; final List targetRestrictions = targetResult.getAccessibilityRestrictions(); if (targetRestrictions != null) { for (final SubtreeAccessibilityRestriction r : targetRestrictions) { if (r.getAccessibilityState() == SubtreeAccessibilityState.ACCESSIBLE) { continue; } final DN restrictionDN; try { restrictionDN = new DN(r.getSubtreeBaseDN()); } catch (final Exception e) { Debug.debugException(e); return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX, ERR_MOVE_SUBTREE_CANNOT_PARSE_RESTRICTION_BASE_DN.get( r.getSubtreeBaseDN(), INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), r.toString(), StaticUtils.getExceptionMessage(e)), null, false, false, 0, 0, 0); } if (restrictionDN.equals(parsedBaseDN)) { targetMatch = true; targetRestriction = r; targetMessage = ERR_MOVE_SUBTREE_NOT_ACCESSIBLE.get(baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), r.getAccessibilityState().getStateName()); break; } else if (restrictionDN.isAncestorOf(parsedBaseDN, false)) { targetRestriction = r; targetMessage = ERR_MOVE_SUBTREE_WITHIN_UNACCESSIBLE_TREE.get(baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); break; } else if (restrictionDN.isDescendantOf(parsedBaseDN, false)) { targetRestriction = r; targetMessage = ERR_MOVE_SUBTREE_CONTAINS_UNACCESSIBLE_TREE.get( baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(), r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName()); break; } } } // If both the source and target servers are available, then we don't need // to do anything else. if ((sourceRestriction == null) && (targetRestriction == null)) { return null; } // If we got a match for both the source and target subtrees, then there's a // good chance that condition results from an interrupted earlier attempt at // running move-subtree. If that's the case, then see if we can provide // specific advice about how to recover. if (sourceMatch || targetMatch) { // If the source is read-only and the target is hidden, then it was // probably in the process of adding entries to the target. Recommend // deleting all entries in the target subtree and making both subtrees // accessible before running again. if ((sourceRestriction != null) && sourceRestriction.getAccessibilityState().isReadOnly() && (targetRestriction != null) && targetRestriction.getAccessibilityState().isHidden()) { return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM, ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_ADDS.get(baseDN, sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort(), targetConnection.getConnectedAddress(), targetConnection.getConnectedPort()), ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_ADDS_ADMIN_MSG.get(), false, false, 0, 0, 0); } // If the source is hidden and the target is accessible, then it was // probably in the process of deleting entries from the source. Recommend // deleting all entries in the source subtree and making the source // subtree accessible. There shouldn't be a need to run again. if ((sourceRestriction != null) && sourceRestriction.getAccessibilityState().isHidden() && (targetRestriction == null)) { return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM, ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_DELETES.get(baseDN, sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort(), targetConnection.getConnectedAddress(), targetConnection.getConnectedPort()), ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_DELETES_ADMIN_MSG.get(), false, false, 0, 0, 0); } } // If we've made it here, then we're in a situation we don't recognize. // Provide general information about the current state of the subtree and // recommend that the user contact support if they need assistance. final StringBuilder details = new StringBuilder(); if (sourceMessage != null) { details.append(sourceMessage); } if (targetMessage != null) { append(targetMessage, details); } return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM, ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED.get(baseDN, sourceConnection.getConnectedAddress(), sourceConnection.getConnectedPort(), targetConnection.getConnectedAddress(), targetConnection.getConnectedPort(), details.toString()), null, false, false, 0, 0, 0); } /** * Updates subtree accessibility in a server. * * @param connection The connection to the server in which the * accessibility state should be applied. * @param isSource Indicates whether the connection is to the source * or target server. * @param baseDN The base DN for the subtree to move. * @param state The accessibility state to apply. * @param bypassDN The DN of a user that will be allowed to bypass * accessibility restrictions. It may be * {@code null} if none is needed. * @param opPurposeControl An optional operation purpose request control * that may be included in the request. * * @throws LDAPException If a problem is encountered while attempting to set * the accessibility state for the subtree. */ private static void setAccessibility(final LDAPConnection connection, final boolean isSource, final String baseDN, final SubtreeAccessibilityState state, final String bypassDN, final OperationPurposeRequestControl opPurposeControl) throws LDAPException { final String connectionName = isSource ? INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get() : INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(); final Control[] controls; if (opPurposeControl == null) { controls = StaticUtils.NO_CONTROLS; } else { controls = new Control[] { opPurposeControl }; } final SetSubtreeAccessibilityExtendedRequest request; switch (state) { case ACCESSIBLE: request = SetSubtreeAccessibilityExtendedRequest. createSetAccessibleRequest(baseDN, controls); break; case READ_ONLY_BIND_ALLOWED: request = SetSubtreeAccessibilityExtendedRequest. createSetReadOnlyRequest(baseDN, true, bypassDN, controls); break; case READ_ONLY_BIND_DENIED: request = SetSubtreeAccessibilityExtendedRequest. createSetReadOnlyRequest(baseDN, false, bypassDN, controls); break; case HIDDEN: request = SetSubtreeAccessibilityExtendedRequest. createSetHiddenRequest(baseDN, bypassDN, controls); break; default: throw new LDAPException(ResultCode.PARAM_ERROR, ERR_MOVE_SUBTREE_UNSUPPORTED_ACCESSIBILITY_STATE.get( state.getStateName(), baseDN, connectionName)); } LDAPResult result; try { result = connection.processExtendedOperation(request); } catch (final LDAPException le) { Debug.debugException(le); result = le.toLDAPResult(); } if (result.getResultCode() != ResultCode.SUCCESS) { throw new LDAPException(result.getResultCode(), ERR_MOVE_SUBTREE_ERROR_SETTING_ACCESSIBILITY.get( state.getStateName(), baseDN, connectionName, result.getDiagnosticMessage())); } } /** * Sets the interrupt message for the given tool, if one was provided. * * @param tool The tool for which to set the interrupt message. It may * be {@code null} if no action should be taken. * @param message The interrupt message to set. It may be {@code null} if * an existing interrupt message should be cleared. */ static void setInterruptMessage(final MoveSubtree tool, final String message) { if (tool != null) { tool.interruptMessage = message; } } /** * Deletes a specified set of entries from the indicated server. * * @param connection The connection to use to communicate with the * server. * @param isSource Indicates whether the connection is to the source * or target server. * @param entryDNs The set of DNs of the entries to be deleted. * @param opPurposeControl An optional operation purpose request control * that may be included in the requests. * @param suppressRefInt Indicates whether to include a request control * causing referential integrity updates to be * suppressed on the source server. * @param listener An optional listener that may be invoked during * the course of moving entries from the source * server to the target server. * @param deleteCount A counter to increment for each delete operation * processed. * @param resultCode A reference to the result code to use for the * move subtree operation. * @param errorMsg A buffer to which any appropriate error messages * may be appended. * * @return {@code true} if the delete was completely successful, or * {@code false} if any errors were encountered. */ private static boolean deleteEntries(final LDAPConnection connection, final boolean isSource, final TreeSet entryDNs, final OperationPurposeRequestControl opPurposeControl, final boolean suppressRefInt, final MoveSubtreeListener listener, final AtomicInteger deleteCount, final AtomicReference resultCode, final StringBuilder errorMsg) { final ArrayList deleteControlList = new ArrayList(3); deleteControlList.add(new ManageDsaITRequestControl(true)); if (opPurposeControl != null) { deleteControlList.add(opPurposeControl); } if (suppressRefInt) { deleteControlList.add( new SuppressReferentialIntegrityUpdatesRequestControl(false)); } final Control[] deleteControls = new Control[deleteControlList.size()]; deleteControlList.toArray(deleteControls); boolean successful = true; for (final DN dn : entryDNs) { if (isSource && (listener != null)) { try { listener.doPreDeleteProcessing(dn); } catch (final Exception e) { Debug.debugException(e); resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); append( ERR_MOVE_SUBTREE_PRE_DELETE_FAILURE.get(dn.toString(), StaticUtils.getExceptionMessage(e)), errorMsg); successful = false; continue; } } LDAPResult deleteResult; try { deleteResult = connection.delete(new DeleteRequest(dn, deleteControls)); } catch (final LDAPException le) { Debug.debugException(le); deleteResult = le.toLDAPResult(); } if (deleteResult.getResultCode() == ResultCode.SUCCESS) { deleteCount.incrementAndGet(); } else { resultCode.compareAndSet(null, deleteResult.getResultCode()); append( ERR_MOVE_SUBTREE_DELETE_FAILURE.get( dn.toString(), deleteResult.getDiagnosticMessage()), errorMsg); successful = false; continue; } if (isSource && (listener != null)) { try { listener.doPostDeleteProcessing(dn); } catch (final Exception e) { Debug.debugException(e); resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); append( ERR_MOVE_SUBTREE_POST_DELETE_FAILURE.get(dn.toString(), StaticUtils.getExceptionMessage(e)), errorMsg); successful = false; } } } return successful; } /** * Appends the provided message to the given buffer. If the buffer is not * empty, then it will insert two spaces before the message. * * @param message The message to be appended to the buffer. * @param buffer The buffer to which the message should be appended. */ static void append(final String message, final StringBuilder buffer) { if (message != null) { if (buffer.length() > 0) { buffer.append(" "); } buffer.append(message); } } /** * {@inheritDoc} */ public void handleUnsolicitedNotification(final LDAPConnection connection, final ExtendedResult notification) { wrapOut(0, 79, INFO_MOVE_SUBTREE_UNSOLICITED_NOTIFICATION.get(notification.getOID(), connection.getConnectionName(), notification.getResultCode(), notification.getDiagnosticMessage())); } /** * {@inheritDoc} */ public ReadOnlyEntry doPreAddProcessing(final ReadOnlyEntry entry) { // No processing required. return entry; } /** * {@inheritDoc} */ public void doPostAddProcessing(final ReadOnlyEntry entry) { wrapOut(0, 79, INFO_MOVE_SUBTREE_ADD_SUCCESSFUL.get(entry.getDN())); } /** * {@inheritDoc} */ public void doPreDeleteProcessing(final DN entryDN) { // No processing required. } /** * {@inheritDoc} */ public void doPostDeleteProcessing(final DN entryDN) { wrapOut(0, 79, INFO_MOVE_SUBTREE_DELETE_SUCCESSFUL.get(entryDN.toString())); } /** * {@inheritDoc} */ @Override() protected boolean registerShutdownHook() { return true; } /** * {@inheritDoc} */ @Override() protected void doShutdownHookProcessing(final ResultCode resultCode) { if (resultCode != null) { // The tool exited normally, so we don't need to do anything. return; } // If there is an interrupt message, then display it. wrapErr(0, 79, interruptMessage); } /** * {@inheritDoc} */ @Override() public LinkedHashMap getExampleUsages() { final LinkedHashMap exampleMap = new LinkedHashMap(1); final String[] args = { "--sourceHostname", "ds1.example.com", "--sourcePort", "389", "--sourceBindDN", "uid=admin,dc=example,dc=com", "--sourceBindPassword", "password", "--targetHostname", "ds2.example.com", "--targetPort", "389", "--targetBindDN", "uid=admin,dc=example,dc=com", "--targetBindPassword", "password", "--baseDN", "cn=small subtree,dc=example,dc=com", "--sizeLimit", "100", "--purpose", "Migrate a small subtree from ds1 to ds2" }; exampleMap.put(args, INFO_MOVE_SUBTREE_EXAMPLE_DESCRIPTION.get()); return exampleMap; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy