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