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