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