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