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