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             false, 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, false, opPurposeControl,
1553         suppressRefInt, listener);
1554  }
1555
1556
1557
1558  /**
1559   * Moves a subtree of entries using a process in which access to the subtree
1560   * will be restricted while the move is in progress.  While entries are being
1561   * read from the source server and added to the target server, the subtree
1562   * will be read-only in the source server and hidden in the target server.
1563   * While entries are being removed from the source server, the subtree will be
1564   * marked as either hidden or to-be-deleted in the source server while fully
1565   * accessible in the target.  After all entries have been removed from the
1566   * source server, the accessibility restriction will be removed from that
1567   * server as well.
1568   * <BR><BR>
1569   * The logic used to accomplish this is as follows:
1570   * <OL>
1571   *   <LI>Make the subtree hidden in the target server.</LI>
1572   *   <LI>Make the subtree read-only in the source server.</LI>
1573   *   <LI>Perform a search in the source server to retrieve all entries in the
1574   *       specified subtree.  The search request will have a subtree scope with
1575   *       a filter of "(objectClass=*)", will include the specified size limit,
1576   *       will request all user and operational attributes, and will include
1577   *       the following request controls:  ManageDsaIT, LDAP subentries,
1578   *       return conflict entries, soft-deleted entry access, real attributes
1579   *       only, and operation purpose.</LI>
1580   *  <LI>For each entry returned by the search, add that entry to the target
1581   *      server.  This method assumes that the source server will return
1582   *      results in a manner that guarantees that no child entry is returned
1583   *      before its parent.  Each add request will include the following
1584   *      controls:  ignore NO-USER-MODIFICATION, and operation purpose.</LI>
1585   *  <LI>Make the subtree read-only in the target server.</LI>
1586   *  <LI>Make the subtree hidden or to-be-deleted in the source server.</LI>
1587   *  <LI>Make the subtree accessible in the target server.</LI>
1588   *  <LI>Delete each entry from the source server, with all subordinate entries
1589   *      before their parents.  Each delete request will include the following
1590   *      controls:  ManageDsaIT, and operation purpose.</LI>
1591   *  <LI>Make the subtree accessible in the source server.</LI>
1592   * </OL>
1593   * Conditions which could result in an incomplete move include:
1594   * <UL>
1595   *   <LI>A failure is encountered while altering the accessibility of the
1596   *       subtree in either the source or target server.</LI>
1597   *   <LI>A failure is encountered while attempting to process an add in the
1598   *       target server and a subsequent failure is encountered when attempting
1599   *       to delete previously-added entries.</LI>
1600   *   <LI>A failure is encountered while attempting to delete one or more
1601   *       entries from the source server.</LI>
1602   * </UL>
1603   *
1604   * @param  sourceConnection  A connection established to the source server.
1605   *                           It should be authenticated as a user with
1606   *                           permission to perform all of the operations
1607   *                           against the source server as referenced above.
1608   * @param  targetConnection  A connection established to the target server.
1609   *                           It should be authenticated as a user with
1610   *                           permission to perform all of the operations
1611   *                           against the target server as referenced above.
1612   * @param  properties        A set of properties that indicate how the move
1613   *                           shoudl be performed.
1614   *
1615   * @return  An object with information about the result of the attempted
1616   *          subtree move.
1617   */
1618  @NotNull()
1619  public static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility(
1620              @NotNull final LDAPConnection sourceConnection,
1621              @NotNull final LDAPConnection targetConnection,
1622              @NotNull final MoveSubtreeProperties properties)
1623  {
1624    return moveSubtreeWithRestrictedAccessibility(null, sourceConnection,
1625         targetConnection, properties.getBaseDN().toString(),
1626         properties.getMaximumAllowedSubtreeSize(),
1627         properties.useToBeDeletedAccessibilityState(),
1628         properties.getOperationPurposeRequestControl(),
1629         properties.suppressReferentialIntegrityUpdates(),
1630         properties.getMoveSubtreeListener());
1631  }
1632
1633
1634
1635  /**
1636   * Performs the real {@code moveSubtreeWithRestrictedAccessibility}
1637   * processing.  If a tool is available, this method will update state
1638   * information in that tool so that it can be referenced by a shutdown hook
1639   * in the event that processing is interrupted.
1640   *
1641   * @param  tool                 A reference to a tool instance to be updated
1642   *                              with state information.
1643   * @param  sourceConnection     A connection established to the source server.
1644   *                              It should be authenticated as a user with
1645   *                              permission to perform all of the operations
1646   *                              against the source server as referenced above.
1647   * @param  targetConnection     A connection established to the target server.
1648   *                              It should be authenticated as a user with
1649   *                              permission to perform all of the operations
1650   *                              against the target server as referenced above.
1651   * @param  baseDN               The base DN for the subtree to move.
1652   * @param  sizeLimit            The maximum number of entries to be moved.  It
1653   *                              may be less than or equal to zero to indicate
1654   *                              that no client-side limit should be enforced
1655   *                              (although the server may still enforce its own
1656   *                              limit).
1657   * @param  useToBeDeletedState  Indicates whether to use the "to be deleted"
1658   *                              accessibility state for the source subtree
1659   *                              once all entries have been copied to the
1660   *                              destination server and the source subtree is
1661   *                              about to be deleted.  The "to be deleted"
1662   *                              state may be more efficient in some cases, but
1663   *                              some Directory Server versions may not support
1664   *                              it.
1665   * @param  opPurposeControl     An optional operation purpose request control
1666   *                              that may be included in all requests sent to
1667   *                              the source and target servers.
1668   * @param  suppressRefInt       Indicates whether to include a request control
1669   *                              causing referential integrity updates to be
1670   *                              suppressed on the source server.
1671   * @param  listener             An optional listener that may be invoked
1672   *                              during the course of moving entries from the
1673   *                              source server to the target server.
1674   *
1675   * @return  An object with information about the result of the attempted
1676   *          subtree move.
1677   */
1678  @NotNull()
1679  private static MoveSubtreeResult moveSubtreeWithRestrictedAccessibility(
1680               @Nullable final MoveSubtree tool,
1681               @NotNull final LDAPConnection sourceConnection,
1682               @NotNull final LDAPConnection targetConnection,
1683               @NotNull final String baseDN, final int sizeLimit,
1684               final boolean useToBeDeletedState,
1685               @Nullable final OperationPurposeRequestControl opPurposeControl,
1686               final boolean suppressRefInt,
1687               @Nullable final MoveSubtreeListener listener)
1688  {
1689    // Ensure that the subtree is currently accessible in both the source and
1690    // target servers.
1691    final MoveSubtreeResult initialAccessibilityResult =
1692         checkInitialAccessibility(sourceConnection, targetConnection, baseDN,
1693              opPurposeControl);
1694    if (initialAccessibilityResult != null)
1695    {
1696      return initialAccessibilityResult;
1697    }
1698
1699
1700    final StringBuilder errorMsg = new StringBuilder();
1701    final StringBuilder adminMsg = new StringBuilder();
1702
1703    final ReverseComparator<DN> reverseComparator = new ReverseComparator<>();
1704    final TreeSet<DN> sourceEntryDNs = new TreeSet<>(reverseComparator);
1705
1706    final AtomicInteger entriesReadFromSource    = new AtomicInteger(0);
1707    final AtomicInteger entriesAddedToTarget     = new AtomicInteger(0);
1708    final AtomicInteger entriesDeletedFromSource = new AtomicInteger(0);
1709    final AtomicReference<ResultCode> resultCode = new AtomicReference<>();
1710
1711    boolean sourceServerAltered = false;
1712    boolean targetServerAltered = false;
1713
1714    SubtreeAccessibilityState currentSourceState =
1715         SubtreeAccessibilityState.ACCESSIBLE;
1716    SubtreeAccessibilityState currentTargetState =
1717         SubtreeAccessibilityState.ACCESSIBLE;
1718
1719processingBlock:
1720    {
1721      // Identify the users authenticated on each connection.
1722      final String sourceUserDN;
1723      final String targetUserDN;
1724      try
1725      {
1726        sourceUserDN = getAuthenticatedUserDN(sourceConnection, true,
1727             opPurposeControl);
1728        targetUserDN = getAuthenticatedUserDN(targetConnection, false,
1729             opPurposeControl);
1730      }
1731      catch (final LDAPException le)
1732      {
1733        Debug.debugException(le);
1734        resultCode.compareAndSet(null, le.getResultCode());
1735        append(le.getMessage(), errorMsg);
1736        break processingBlock;
1737      }
1738
1739
1740      // Make the subtree hidden on the target server.
1741      try
1742      {
1743        setAccessibility(targetConnection, false, baseDN,
1744             SubtreeAccessibilityState.HIDDEN, targetUserDN, opPurposeControl);
1745        currentTargetState = SubtreeAccessibilityState.HIDDEN;
1746        setInterruptMessage(tool,
1747             WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_HIDDEN.get(baseDN,
1748                  targetConnection.getConnectedAddress(),
1749                  targetConnection.getConnectedPort()));
1750      }
1751      catch (final LDAPException le)
1752      {
1753        Debug.debugException(le);
1754        resultCode.compareAndSet(null, le.getResultCode());
1755        append(le.getMessage(), errorMsg);
1756        break processingBlock;
1757      }
1758
1759
1760      // Make the subtree read-only on the source server.
1761      try
1762      {
1763        setAccessibility(sourceConnection, true, baseDN,
1764             SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, sourceUserDN,
1765             opPurposeControl);
1766        currentSourceState = SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED;
1767        setInterruptMessage(tool,
1768             WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_READ_ONLY.get(baseDN,
1769                  targetConnection.getConnectedAddress(),
1770                  targetConnection.getConnectedPort(),
1771                  sourceConnection.getConnectedAddress(),
1772                  sourceConnection.getConnectedPort()));
1773      }
1774      catch (final LDAPException le)
1775      {
1776        Debug.debugException(le);
1777        resultCode.compareAndSet(null, le.getResultCode());
1778        append(le.getMessage(), errorMsg);
1779        break processingBlock;
1780      }
1781
1782
1783      // Perform a search to find all entries in the target subtree, and include
1784      // a search listener that will add each entry to the target server as it
1785      // is returned from the source server.
1786      final Control[] searchControls;
1787      if (opPurposeControl == null)
1788      {
1789        searchControls = new Control[]
1790        {
1791          new DraftLDUPSubentriesRequestControl(true),
1792          new ManageDsaITRequestControl(true),
1793          new ReturnConflictEntriesRequestControl(true),
1794          new SoftDeletedEntryAccessRequestControl(true, true, false),
1795          new RealAttributesOnlyRequestControl(true)
1796        };
1797      }
1798      else
1799      {
1800        searchControls = new Control[]
1801        {
1802          new DraftLDUPSubentriesRequestControl(true),
1803          new ManageDsaITRequestControl(true),
1804          new ReturnConflictEntriesRequestControl(true),
1805          new SoftDeletedEntryAccessRequestControl(true, true, false),
1806          new RealAttributesOnlyRequestControl(true),
1807          opPurposeControl
1808        };
1809      }
1810
1811      final MoveSubtreeAccessibilitySearchListener searchListener =
1812           new MoveSubtreeAccessibilitySearchListener(tool, baseDN,
1813                sourceConnection, targetConnection, resultCode, errorMsg,
1814                entriesReadFromSource, entriesAddedToTarget, sourceEntryDNs,
1815                opPurposeControl, listener);
1816      final SearchRequest searchRequest = new SearchRequest(
1817           searchListener, searchControls, baseDN, SearchScope.SUB,
1818           DereferencePolicy.NEVER, sizeLimit, 0, false,
1819           Filter.createPresenceFilter("objectClass"), "*", "+");
1820
1821      SearchResult searchResult;
1822      try
1823      {
1824        searchResult = sourceConnection.search(searchRequest);
1825      }
1826      catch (final LDAPSearchException lse)
1827      {
1828        Debug.debugException(lse);
1829        searchResult = lse.getSearchResult();
1830      }
1831
1832      if (entriesAddedToTarget.get() > 0)
1833      {
1834        targetServerAltered = true;
1835      }
1836
1837      if (searchResult.getResultCode() != ResultCode.SUCCESS)
1838      {
1839        resultCode.compareAndSet(null, searchResult.getResultCode());
1840        append(
1841             ERR_MOVE_SUBTREE_SEARCH_FAILED.get(baseDN,
1842                  searchResult.getDiagnosticMessage()),
1843             errorMsg);
1844
1845        final AtomicInteger deleteCount = new AtomicInteger(0);
1846        if (targetServerAltered)
1847        {
1848          deleteEntries(targetConnection, false, sourceEntryDNs,
1849               opPurposeControl, false, null, deleteCount, resultCode,
1850               errorMsg);
1851          entriesAddedToTarget.addAndGet(0 - deleteCount.get());
1852          if (entriesAddedToTarget.get() == 0)
1853          {
1854            targetServerAltered = false;
1855          }
1856          else
1857          {
1858            append(ERR_MOVE_SUBTREE_TARGET_NOT_DELETED_ADMIN_ACTION.get(baseDN),
1859                 adminMsg);
1860          }
1861        }
1862        break processingBlock;
1863      }
1864
1865      // If an error occurred during add processing, then fail.
1866      if (resultCode.get() != null)
1867      {
1868        final AtomicInteger deleteCount = new AtomicInteger(0);
1869        if (targetServerAltered)
1870        {
1871          deleteEntries(targetConnection, false, sourceEntryDNs,
1872               opPurposeControl, false, null, deleteCount, resultCode,
1873               errorMsg);
1874          entriesAddedToTarget.addAndGet(0 - deleteCount.get());
1875          if (entriesAddedToTarget.get() == 0)
1876          {
1877            targetServerAltered = false;
1878          }
1879          else
1880          {
1881            append(ERR_MOVE_SUBTREE_TARGET_NOT_DELETED_ADMIN_ACTION.get(baseDN),
1882                 adminMsg);
1883          }
1884        }
1885        break processingBlock;
1886      }
1887
1888
1889      // Make the subtree read-only on the target server.
1890      try
1891      {
1892        setAccessibility(targetConnection, true, baseDN,
1893             SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED, targetUserDN,
1894             opPurposeControl);
1895        currentTargetState = SubtreeAccessibilityState.READ_ONLY_BIND_ALLOWED;
1896        setInterruptMessage(tool,
1897             WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_READ_ONLY.get(baseDN,
1898                  sourceConnection.getConnectedAddress(),
1899                  sourceConnection.getConnectedPort(),
1900                  targetConnection.getConnectedAddress(),
1901                  targetConnection.getConnectedPort()));
1902      }
1903      catch (final LDAPException le)
1904      {
1905        Debug.debugException(le);
1906        resultCode.compareAndSet(null, le.getResultCode());
1907        append(le.getMessage(), errorMsg);
1908        break processingBlock;
1909      }
1910
1911
1912      // Make the subtree either hidden or to-be-deleted on the source server.
1913      try
1914      {
1915        if (useToBeDeletedState)
1916        {
1917          setAccessibility(sourceConnection, true, baseDN,
1918               SubtreeAccessibilityState.TO_BE_DELETED, sourceUserDN,
1919               opPurposeControl);
1920          currentSourceState = SubtreeAccessibilityState.HIDDEN;
1921          setInterruptMessage(tool,
1922               WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_TO_BE_DELETED.get(baseDN,
1923                    sourceConnection.getConnectedAddress(),
1924                    sourceConnection.getConnectedPort(),
1925                    targetConnection.getConnectedAddress(),
1926                    targetConnection.getConnectedPort()));
1927        }
1928        else
1929        {
1930          setAccessibility(sourceConnection, true, baseDN,
1931               SubtreeAccessibilityState.HIDDEN, sourceUserDN,
1932               opPurposeControl);
1933          currentSourceState = SubtreeAccessibilityState.HIDDEN;
1934          setInterruptMessage(tool,
1935               WARN_MOVE_SUBTREE_INTERRUPT_MSG_SOURCE_HIDDEN.get(baseDN,
1936                    sourceConnection.getConnectedAddress(),
1937                    sourceConnection.getConnectedPort(),
1938                    targetConnection.getConnectedAddress(),
1939                    targetConnection.getConnectedPort()));
1940        }
1941      }
1942      catch (final LDAPException le)
1943      {
1944        Debug.debugException(le);
1945        resultCode.compareAndSet(null, le.getResultCode());
1946        append(le.getMessage(), errorMsg);
1947        break processingBlock;
1948      }
1949
1950
1951      // Make the subtree accessible on the target server.
1952      try
1953      {
1954        setAccessibility(targetConnection, true, baseDN,
1955             SubtreeAccessibilityState.ACCESSIBLE, targetUserDN,
1956             opPurposeControl);
1957        currentTargetState = SubtreeAccessibilityState.ACCESSIBLE;
1958        setInterruptMessage(tool,
1959             WARN_MOVE_SUBTREE_INTERRUPT_MSG_TARGET_ACCESSIBLE.get(baseDN,
1960                  sourceConnection.getConnectedAddress(),
1961                  sourceConnection.getConnectedPort(),
1962                  targetConnection.getConnectedAddress(),
1963                  targetConnection.getConnectedPort()));
1964      }
1965      catch (final LDAPException le)
1966      {
1967        Debug.debugException(le);
1968        resultCode.compareAndSet(null, le.getResultCode());
1969        append(le.getMessage(), errorMsg);
1970        break processingBlock;
1971      }
1972
1973
1974      // Delete each of the entries in the source server.  The map should
1975      // already be sorted in reverse order (as a result of the comparator used
1976      // when creating it), so it will guarantee children are deleted before
1977      // their parents.
1978      final boolean deleteSuccessful = deleteEntries(sourceConnection, true,
1979           sourceEntryDNs, opPurposeControl, suppressRefInt, listener,
1980           entriesDeletedFromSource, resultCode, errorMsg);
1981      sourceServerAltered = (entriesDeletedFromSource.get() != 0);
1982      if (! deleteSuccessful)
1983      {
1984        append(ERR_MOVE_SUBTREE_SOURCE_NOT_DELETED_ADMIN_ACTION.get(baseDN),
1985             adminMsg);
1986        break processingBlock;
1987      }
1988
1989
1990      // If the source subtree is hidden, then make it accessible.  This isn't
1991      // needed if it's in a to-be-deleted state because that state should have
1992      // automatically been cleared upon deleting the base entry.
1993      try
1994      {
1995        if (! useToBeDeletedState)
1996        {
1997          setAccessibility(sourceConnection, true, baseDN,
1998               SubtreeAccessibilityState.ACCESSIBLE, sourceUserDN,
1999               opPurposeControl);
2000        }
2001
2002        currentSourceState = SubtreeAccessibilityState.ACCESSIBLE;
2003        setInterruptMessage(tool, null);
2004      }
2005      catch (final LDAPException le)
2006      {
2007        Debug.debugException(le);
2008        resultCode.compareAndSet(null, le.getResultCode());
2009        append(le.getMessage(), errorMsg);
2010        break processingBlock;
2011      }
2012    }
2013
2014
2015    // If the source server was left in a state other than accessible, then
2016    // see if we can safely change it back.  If it's left in any state other
2017    // than accessible, then generate an admin action message.
2018    if (currentSourceState != SubtreeAccessibilityState.ACCESSIBLE)
2019    {
2020      if (! sourceServerAltered)
2021      {
2022        try
2023        {
2024          setAccessibility(sourceConnection, true, baseDN,
2025               SubtreeAccessibilityState.ACCESSIBLE, null, opPurposeControl);
2026          currentSourceState = SubtreeAccessibilityState.ACCESSIBLE;
2027        }
2028        catch (final LDAPException le)
2029        {
2030          Debug.debugException(le);
2031        }
2032      }
2033
2034      if (currentSourceState != SubtreeAccessibilityState.ACCESSIBLE)
2035      {
2036        append(
2037             ERR_MOVE_SUBTREE_SOURCE_LEFT_INACCESSIBLE.get(
2038                  currentSourceState, baseDN),
2039             adminMsg);
2040      }
2041    }
2042
2043
2044    // If the target server was left in a state other than accessible, then
2045    // see if we can safely change it back.  If it's left in any state other
2046    // than accessible, then generate an admin action message.
2047    if (currentTargetState != SubtreeAccessibilityState.ACCESSIBLE)
2048    {
2049      if (! targetServerAltered)
2050      {
2051        try
2052        {
2053          setAccessibility(targetConnection, false, baseDN,
2054               SubtreeAccessibilityState.ACCESSIBLE, null, opPurposeControl);
2055          currentTargetState = SubtreeAccessibilityState.ACCESSIBLE;
2056        }
2057        catch (final LDAPException le)
2058        {
2059          Debug.debugException(le);
2060        }
2061      }
2062
2063      if (currentTargetState != SubtreeAccessibilityState.ACCESSIBLE)
2064      {
2065        append(
2066             ERR_MOVE_SUBTREE_TARGET_LEFT_INACCESSIBLE.get(
2067                  currentTargetState, baseDN),
2068             adminMsg);
2069      }
2070    }
2071
2072
2073    // Construct the result to return to the client.
2074    resultCode.compareAndSet(null, ResultCode.SUCCESS);
2075
2076    final String errorMessage;
2077    if (errorMsg.length() > 0)
2078    {
2079      errorMessage = errorMsg.toString();
2080    }
2081    else
2082    {
2083      errorMessage = null;
2084    }
2085
2086    final String adminActionRequired;
2087    if (adminMsg.length() > 0)
2088    {
2089      adminActionRequired = adminMsg.toString();
2090    }
2091    else
2092    {
2093      adminActionRequired = null;
2094    }
2095
2096    return new MoveSubtreeResult(resultCode.get(), errorMessage,
2097         adminActionRequired, sourceServerAltered, targetServerAltered,
2098         entriesReadFromSource.get(), entriesAddedToTarget.get(),
2099         entriesDeletedFromSource.get());
2100  }
2101
2102
2103
2104  /**
2105   * Retrieves the DN of the user authenticated on the provided connection.  It
2106   * will first try to look at the last successful bind request processed on the
2107   * connection, and will fall back to using the "Who Am I?" extended request.
2108   *
2109   * @param  connection        The connection for which to make the
2110   *                           determination.
2111   * @param  isSource          Indicates whether the connection is to the source
2112   *                           or target server.
2113   * @param  opPurposeControl  An optional operation purpose request control
2114   *                           that may be included in the request.
2115   *
2116   * @return  The DN of the user authenticated on the provided connection, or
2117   *          {@code null} if the connection is not authenticated.
2118   *
2119   * @throws  LDAPException  If a problem is encountered while making the
2120   *                         determination.
2121   */
2122  @Nullable()
2123  private static String getAuthenticatedUserDN(
2124               @NotNull final LDAPConnection connection,
2125               final boolean isSource,
2126               @Nullable final OperationPurposeRequestControl opPurposeControl)
2127          throws LDAPException
2128  {
2129    final BindRequest bindRequest =
2130         InternalSDKHelper.getLastBindRequest(connection);
2131    if ((bindRequest != null) && (bindRequest instanceof SimpleBindRequest))
2132    {
2133      final SimpleBindRequest r = (SimpleBindRequest) bindRequest;
2134      return r.getBindDN();
2135    }
2136
2137
2138    final Control[] controls;
2139    if (opPurposeControl == null)
2140    {
2141      controls = StaticUtils.NO_CONTROLS;
2142    }
2143    else
2144    {
2145      controls = new Control[]
2146      {
2147        opPurposeControl
2148      };
2149    }
2150
2151    final String connectionName =
2152         isSource
2153         ? INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get()
2154         : INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get();
2155
2156    final WhoAmIExtendedResult whoAmIResult;
2157    try
2158    {
2159      whoAmIResult = (WhoAmIExtendedResult)
2160           connection.processExtendedOperation(
2161                new WhoAmIExtendedRequest(controls));
2162    }
2163    catch (final LDAPException le)
2164    {
2165      Debug.debugException(le);
2166      throw new LDAPException(le.getResultCode(),
2167           ERR_MOVE_SUBTREE_ERROR_INVOKING_WHO_AM_I.get(connectionName,
2168                StaticUtils.getExceptionMessage(le)),
2169           le);
2170    }
2171
2172    if (whoAmIResult.getResultCode() != ResultCode.SUCCESS)
2173    {
2174      throw new LDAPException(whoAmIResult.getResultCode(),
2175           ERR_MOVE_SUBTREE_ERROR_INVOKING_WHO_AM_I.get(connectionName,
2176                whoAmIResult.getDiagnosticMessage()));
2177    }
2178
2179    final String authzID = whoAmIResult.getAuthorizationID();
2180    if ((authzID != null) && authzID.startsWith("dn:"))
2181    {
2182      return authzID.substring(3);
2183    }
2184    else
2185    {
2186      throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
2187           ERR_MOVE_SUBTREE_CANNOT_IDENTIFY_CONNECTED_USER.get(connectionName));
2188    }
2189  }
2190
2191
2192
2193  /**
2194   * Ensures that the specified subtree is accessible in both the source and
2195   * target servers.  If it is not accessible, then it may indicate that another
2196   * administrative operation is in progress for the subtree, or that a previous
2197   * move-subtree operation was interrupted before it could complete.
2198   *
2199   * @param  sourceConnection  The connection to use to communicate with the
2200   *                           source directory server.
2201   * @param  targetConnection  The connection to use to communicate with the
2202   *                           target directory server.
2203   * @param  baseDN            The base DN for which to verify accessibility.
2204   * @param  opPurposeControl  An optional operation purpose request control
2205   *                           that may be included in the requests.
2206   *
2207   * @return  {@code null} if the specified subtree is accessible in both the
2208   *          source and target servers, or a non-{@code null} object with the
2209   *          result that should be used if there is an accessibility problem
2210   *          with the subtree on the source and/or target server.
2211   */
2212  @Nullable()
2213  private static MoveSubtreeResult checkInitialAccessibility(
2214               @NotNull final LDAPConnection sourceConnection,
2215               @NotNull final LDAPConnection targetConnection,
2216               @NotNull final String baseDN,
2217               @Nullable final OperationPurposeRequestControl opPurposeControl)
2218  {
2219    final DN parsedBaseDN;
2220    try
2221    {
2222      parsedBaseDN = new DN(baseDN);
2223    }
2224    catch (final Exception e)
2225    {
2226      Debug.debugException(e);
2227      return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX,
2228           ERR_MOVE_SUBTREE_CANNOT_PARSE_BASE_DN.get(baseDN,
2229                StaticUtils.getExceptionMessage(e)),
2230           null, false, false, 0, 0, 0);
2231    }
2232
2233    final Control[] controls;
2234    if (opPurposeControl == null)
2235    {
2236      controls = StaticUtils.NO_CONTROLS;
2237    }
2238    else
2239    {
2240      controls = new Control[]
2241      {
2242        opPurposeControl
2243      };
2244    }
2245
2246
2247    // Get the restrictions from the source server.  If there are any, then
2248    // make sure that nothing in the hierarchy of the base DN is non-accessible.
2249    final GetSubtreeAccessibilityExtendedResult sourceResult;
2250    try
2251    {
2252      sourceResult = (GetSubtreeAccessibilityExtendedResult)
2253           sourceConnection.processExtendedOperation(
2254                new GetSubtreeAccessibilityExtendedRequest(controls));
2255      if (sourceResult.getResultCode() != ResultCode.SUCCESS)
2256      {
2257        throw new LDAPException(sourceResult);
2258      }
2259    }
2260    catch (final LDAPException le)
2261    {
2262      Debug.debugException(le);
2263      return new MoveSubtreeResult(le.getResultCode(),
2264           ERR_MOVE_SUBTREE_CANNOT_GET_ACCESSIBILITY_STATE.get(baseDN,
2265                INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(),
2266                le.getMessage()),
2267           null, false, false, 0, 0, 0);
2268    }
2269
2270    boolean sourceMatch = false;
2271    String sourceMessage = null;
2272    SubtreeAccessibilityRestriction sourceRestriction = null;
2273    final List<SubtreeAccessibilityRestriction> sourceRestrictions =
2274         sourceResult.getAccessibilityRestrictions();
2275    if (sourceRestrictions != null)
2276    {
2277      for (final SubtreeAccessibilityRestriction r : sourceRestrictions)
2278      {
2279        if (r.getAccessibilityState() == SubtreeAccessibilityState.ACCESSIBLE)
2280        {
2281          continue;
2282        }
2283
2284        final DN restrictionDN;
2285        try
2286        {
2287          restrictionDN = new DN(r.getSubtreeBaseDN());
2288        }
2289        catch (final Exception e)
2290        {
2291          Debug.debugException(e);
2292          return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX,
2293               ERR_MOVE_SUBTREE_CANNOT_PARSE_RESTRICTION_BASE_DN.get(
2294                    r.getSubtreeBaseDN(),
2295                    INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(),
2296                    r.toString(), StaticUtils.getExceptionMessage(e)),
2297               null, false, false, 0, 0, 0);
2298        }
2299
2300        if (restrictionDN.equals(parsedBaseDN))
2301        {
2302          sourceMatch = true;
2303          sourceRestriction = r;
2304          sourceMessage = ERR_MOVE_SUBTREE_NOT_ACCESSIBLE.get(baseDN,
2305               INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(),
2306               r.getAccessibilityState().getStateName());
2307          break;
2308        }
2309        else if (restrictionDN.isAncestorOf(parsedBaseDN, false))
2310        {
2311          sourceRestriction = r;
2312          sourceMessage = ERR_MOVE_SUBTREE_WITHIN_UNACCESSIBLE_TREE.get(baseDN,
2313               INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(),
2314               r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName());
2315          break;
2316        }
2317        else if (restrictionDN.isDescendantOf(parsedBaseDN, false))
2318        {
2319          sourceRestriction = r;
2320          sourceMessage = ERR_MOVE_SUBTREE_CONTAINS_UNACCESSIBLE_TREE.get(
2321               baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get(),
2322               r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName());
2323          break;
2324        }
2325      }
2326    }
2327
2328
2329    // Get the restrictions from the target server.  If there are any, then
2330    // make sure that nothing in the hierarchy of the base DN is non-accessible.
2331    final GetSubtreeAccessibilityExtendedResult targetResult;
2332    try
2333    {
2334      targetResult = (GetSubtreeAccessibilityExtendedResult)
2335           targetConnection.processExtendedOperation(
2336                new GetSubtreeAccessibilityExtendedRequest(controls));
2337      if (targetResult.getResultCode() != ResultCode.SUCCESS)
2338      {
2339        throw new LDAPException(targetResult);
2340      }
2341    }
2342    catch (final LDAPException le)
2343    {
2344      Debug.debugException(le);
2345      return new MoveSubtreeResult(le.getResultCode(),
2346           ERR_MOVE_SUBTREE_CANNOT_GET_ACCESSIBILITY_STATE.get(baseDN,
2347                INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(),
2348                le.getMessage()),
2349           null, false, false, 0, 0, 0);
2350    }
2351
2352    boolean targetMatch = false;
2353    String targetMessage = null;
2354    SubtreeAccessibilityRestriction targetRestriction = null;
2355    final List<SubtreeAccessibilityRestriction> targetRestrictions =
2356         targetResult.getAccessibilityRestrictions();
2357    if (targetRestrictions != null)
2358    {
2359      for (final SubtreeAccessibilityRestriction r : targetRestrictions)
2360      {
2361        if (r.getAccessibilityState() == SubtreeAccessibilityState.ACCESSIBLE)
2362        {
2363          continue;
2364        }
2365
2366        final DN restrictionDN;
2367        try
2368        {
2369          restrictionDN = new DN(r.getSubtreeBaseDN());
2370        }
2371        catch (final Exception e)
2372        {
2373          Debug.debugException(e);
2374          return new MoveSubtreeResult(ResultCode.INVALID_DN_SYNTAX,
2375               ERR_MOVE_SUBTREE_CANNOT_PARSE_RESTRICTION_BASE_DN.get(
2376                    r.getSubtreeBaseDN(),
2377                    INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(),
2378                    r.toString(), StaticUtils.getExceptionMessage(e)),
2379               null, false, false, 0, 0, 0);
2380        }
2381
2382        if (restrictionDN.equals(parsedBaseDN))
2383        {
2384          targetMatch = true;
2385          targetRestriction = r;
2386          targetMessage = ERR_MOVE_SUBTREE_NOT_ACCESSIBLE.get(baseDN,
2387               INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(),
2388               r.getAccessibilityState().getStateName());
2389          break;
2390        }
2391        else if (restrictionDN.isAncestorOf(parsedBaseDN, false))
2392        {
2393          targetRestriction = r;
2394          targetMessage = ERR_MOVE_SUBTREE_WITHIN_UNACCESSIBLE_TREE.get(baseDN,
2395               INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(),
2396               r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName());
2397          break;
2398        }
2399        else if (restrictionDN.isDescendantOf(parsedBaseDN, false))
2400        {
2401          targetRestriction = r;
2402          targetMessage = ERR_MOVE_SUBTREE_CONTAINS_UNACCESSIBLE_TREE.get(
2403               baseDN, INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get(),
2404               r.getSubtreeBaseDN(), r.getAccessibilityState().getStateName());
2405          break;
2406        }
2407      }
2408    }
2409
2410
2411    // If both the source and target servers are available, then we don't need
2412    // to do anything else.
2413    if ((sourceRestriction == null) && (targetRestriction == null))
2414    {
2415      return null;
2416    }
2417
2418
2419    // If we got a match for both the source and target subtrees, then there's a
2420    // good chance that condition results from an interrupted earlier attempt at
2421    // running move-subtree.  If that's the case, then see if we can provide
2422    // specific advice about how to recover.
2423    if (sourceMatch || targetMatch)
2424    {
2425      // If the source is read-only and the target is hidden, then it was
2426      // probably in the process of adding entries to the target.  Recommend
2427      // deleting all entries in the target subtree and making both subtrees
2428      // accessible before running again.
2429      if ((sourceRestriction != null) &&
2430          sourceRestriction.getAccessibilityState().isReadOnly() &&
2431          (targetRestriction != null) &&
2432          targetRestriction.getAccessibilityState().isHidden())
2433      {
2434        return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM,
2435             ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_ADDS.get(baseDN,
2436                  sourceConnection.getConnectedAddress(),
2437                  sourceConnection.getConnectedPort(),
2438                  targetConnection.getConnectedAddress(),
2439                  targetConnection.getConnectedPort()),
2440             ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_ADDS_ADMIN_MSG.get(),
2441             false, false, 0, 0, 0);
2442      }
2443
2444
2445      // If the source is hidden and the target is accessible, then it was
2446      // probably in the process of deleting entries from the source.  Recommend
2447      // deleting all entries in the source subtree and making the source
2448      // subtree accessible.  There shouldn't be a need to run again.
2449      if ((sourceRestriction != null) &&
2450          sourceRestriction.getAccessibilityState().isHidden() &&
2451          (targetRestriction == null))
2452      {
2453        return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM,
2454             ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_DELETES.get(baseDN,
2455                  sourceConnection.getConnectedAddress(),
2456                  sourceConnection.getConnectedPort(),
2457                  targetConnection.getConnectedAddress(),
2458                  targetConnection.getConnectedPort()),
2459             ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED_IN_DELETES_ADMIN_MSG.get(),
2460             false, false, 0, 0, 0);
2461      }
2462    }
2463
2464
2465    // If we've made it here, then we're in a situation we don't recognize.
2466    // Provide general information about the current state of the subtree and
2467    // recommend that the user contact support if they need assistance.
2468    final StringBuilder details = new StringBuilder();
2469    if (sourceMessage != null)
2470    {
2471      details.append(sourceMessage);
2472    }
2473    if (targetMessage != null)
2474    {
2475      append(targetMessage, details);
2476    }
2477    return new MoveSubtreeResult(ResultCode.UNWILLING_TO_PERFORM,
2478         ERR_MOVE_SUBTREE_POSSIBLY_INTERRUPTED.get(baseDN,
2479              sourceConnection.getConnectedAddress(),
2480              sourceConnection.getConnectedPort(),
2481              targetConnection.getConnectedAddress(),
2482              targetConnection.getConnectedPort(), details.toString()),
2483         null, false, false, 0, 0, 0);
2484  }
2485
2486
2487
2488  /**
2489   * Updates subtree accessibility in a server.
2490   *
2491   * @param  connection        The connection to the server in which the
2492   *                           accessibility state should be applied.
2493   * @param  isSource          Indicates whether the connection is to the source
2494   *                           or target server.
2495   * @param  baseDN            The base DN for the subtree to move.
2496   * @param  state             The accessibility state to apply.
2497   * @param  bypassDN          The DN of a user that will be allowed to bypass
2498   *                           accessibility restrictions.  It may be
2499   *                           {@code null} if none is needed.
2500   * @param  opPurposeControl  An optional operation purpose request control
2501   *                           that may be included in the request.
2502   *
2503   * @throws  LDAPException  If a problem is encountered while attempting to set
2504   *                         the accessibility state for the subtree.
2505   */
2506  private static void setAccessibility(
2507               @NotNull final LDAPConnection connection,
2508               final boolean isSource,
2509               @NotNull final String baseDN,
2510               @NotNull final SubtreeAccessibilityState state,
2511               @Nullable final String bypassDN,
2512               @Nullable final OperationPurposeRequestControl opPurposeControl)
2513          throws LDAPException
2514  {
2515    final String connectionName =
2516         isSource
2517         ? INFO_MOVE_SUBTREE_CONNECTION_NAME_SOURCE.get()
2518         : INFO_MOVE_SUBTREE_CONNECTION_NAME_TARGET.get();
2519
2520    final Control[] controls;
2521    if (opPurposeControl == null)
2522    {
2523      controls = StaticUtils.NO_CONTROLS;
2524    }
2525    else
2526    {
2527      controls = new Control[]
2528      {
2529        opPurposeControl
2530      };
2531    }
2532
2533    final SetSubtreeAccessibilityExtendedRequest request;
2534    switch (state)
2535    {
2536      case ACCESSIBLE:
2537        request = SetSubtreeAccessibilityExtendedRequest.
2538             createSetAccessibleRequest(baseDN, controls);
2539        break;
2540      case READ_ONLY_BIND_ALLOWED:
2541        request = SetSubtreeAccessibilityExtendedRequest.
2542             createSetReadOnlyRequest(baseDN, true, bypassDN, controls);
2543        break;
2544      case READ_ONLY_BIND_DENIED:
2545        request = SetSubtreeAccessibilityExtendedRequest.
2546             createSetReadOnlyRequest(baseDN, false, bypassDN, controls);
2547        break;
2548      case HIDDEN:
2549        request = SetSubtreeAccessibilityExtendedRequest.
2550             createSetHiddenRequest(baseDN, bypassDN, controls);
2551        break;
2552      case TO_BE_DELETED:
2553        request = SetSubtreeAccessibilityExtendedRequest.
2554             createSetToBeDeletedRequest(baseDN, bypassDN, controls);
2555        break;
2556      default:
2557        throw new LDAPException(ResultCode.PARAM_ERROR,
2558             ERR_MOVE_SUBTREE_UNSUPPORTED_ACCESSIBILITY_STATE.get(
2559                  state.getStateName(), baseDN, connectionName));
2560    }
2561
2562    LDAPResult result;
2563    try
2564    {
2565      result = connection.processExtendedOperation(request);
2566    }
2567    catch (final LDAPException le)
2568    {
2569      Debug.debugException(le);
2570      result = le.toLDAPResult();
2571    }
2572
2573    if (result.getResultCode() != ResultCode.SUCCESS)
2574    {
2575      throw new LDAPException(result.getResultCode(),
2576           ERR_MOVE_SUBTREE_ERROR_SETTING_ACCESSIBILITY.get(
2577                state.getStateName(), baseDN, connectionName,
2578                result.getDiagnosticMessage()));
2579    }
2580  }
2581
2582
2583
2584  /**
2585   * Sets the interrupt message for the given tool, if one was provided.
2586   *
2587   * @param  tool     The tool for which to set the interrupt message.  It may
2588   *                  be {@code null} if no action should be taken.
2589   * @param  message  The interrupt message to set.  It may be {@code null} if
2590   *                  an existing interrupt message should be cleared.
2591   */
2592  static void setInterruptMessage(@Nullable final MoveSubtree tool,
2593                                  @Nullable final String message)
2594  {
2595    if (tool != null)
2596    {
2597      tool.interruptMessage = message;
2598    }
2599  }
2600
2601
2602
2603  /**
2604   * Deletes a specified set of entries from the indicated server.
2605   *
2606   * @param  connection        The connection to use to communicate with the
2607   *                           server.
2608   * @param  isSource          Indicates whether the connection is to the source
2609   *                           or target server.
2610   * @param  entryDNs          The set of DNs of the entries to be deleted.
2611   * @param  opPurposeControl  An optional operation purpose request control
2612   *                           that may be included in the requests.
2613   * @param  suppressRefInt    Indicates whether to include a request control
2614   *                           causing referential integrity updates to be
2615   *                           suppressed on the source server.
2616   * @param  listener          An optional listener that may be invoked during
2617   *                           the course of moving entries from the source
2618   *                           server to the target server.
2619   * @param  deleteCount       A counter to increment for each delete operation
2620   *                           processed.
2621   * @param  resultCode        A reference to the result code to use for the
2622   *                           move subtree operation.
2623   * @param  errorMsg          A buffer to which any appropriate error messages
2624   *                           may be appended.
2625   *
2626   * @return  {@code true} if the delete was completely successful, or
2627   *          {@code false} if any errors were encountered.
2628   */
2629  private static boolean deleteEntries(
2630               @NotNull final LDAPConnection connection,
2631               final boolean isSource,
2632               @NotNull final TreeSet<DN> entryDNs,
2633               @Nullable final OperationPurposeRequestControl opPurposeControl,
2634               final boolean suppressRefInt,
2635               @Nullable final MoveSubtreeListener listener,
2636               @NotNull final AtomicInteger deleteCount,
2637               @NotNull final AtomicReference<ResultCode> resultCode,
2638               @NotNull final StringBuilder errorMsg)
2639  {
2640    final ArrayList<Control> deleteControlList = new ArrayList<>(3);
2641    deleteControlList.add(new ManageDsaITRequestControl(true));
2642    if (opPurposeControl != null)
2643    {
2644      deleteControlList.add(opPurposeControl);
2645    }
2646    if (suppressRefInt)
2647    {
2648      deleteControlList.add(
2649           new SuppressReferentialIntegrityUpdatesRequestControl(false));
2650    }
2651
2652    final Control[] deleteControls = new Control[deleteControlList.size()];
2653    deleteControlList.toArray(deleteControls);
2654
2655    boolean successful = true;
2656    for (final DN dn : entryDNs)
2657    {
2658      if (isSource && (listener != null))
2659      {
2660        try
2661        {
2662          listener.doPreDeleteProcessing(dn);
2663        }
2664        catch (final Exception e)
2665        {
2666          Debug.debugException(e);
2667          resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
2668          append(
2669               ERR_MOVE_SUBTREE_PRE_DELETE_FAILURE.get(dn.toString(),
2670                    StaticUtils.getExceptionMessage(e)),
2671               errorMsg);
2672          successful = false;
2673          continue;
2674        }
2675      }
2676
2677      LDAPResult deleteResult;
2678      try
2679      {
2680        deleteResult = connection.delete(new DeleteRequest(dn, deleteControls));
2681      }
2682      catch (final LDAPException le)
2683      {
2684        Debug.debugException(le);
2685        deleteResult = le.toLDAPResult();
2686      }
2687
2688      if (deleteResult.getResultCode() == ResultCode.SUCCESS)
2689      {
2690        deleteCount.incrementAndGet();
2691      }
2692      else
2693      {
2694        resultCode.compareAndSet(null, deleteResult.getResultCode());
2695        append(
2696            ERR_MOVE_SUBTREE_DELETE_FAILURE.get(
2697                dn.toString(),
2698                deleteResult.getDiagnosticMessage()),
2699            errorMsg);
2700        successful = false;
2701        continue;
2702      }
2703
2704      if (isSource && (listener != null))
2705      {
2706        try
2707        {
2708          listener.doPostDeleteProcessing(dn);
2709        }
2710        catch (final Exception e)
2711        {
2712          Debug.debugException(e);
2713          resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR);
2714          append(
2715               ERR_MOVE_SUBTREE_POST_DELETE_FAILURE.get(dn.toString(),
2716                    StaticUtils.getExceptionMessage(e)),
2717               errorMsg);
2718          successful = false;
2719        }
2720      }
2721    }
2722
2723    return successful;
2724  }
2725
2726
2727
2728  /**
2729   * Appends the provided message to the given buffer.  If the buffer is not
2730   * empty, then it will insert two spaces before the message.
2731   *
2732   * @param  message  The message to be appended to the buffer.
2733   * @param  buffer   The buffer to which the message should be appended.
2734   */
2735  static void append(@Nullable final String message,
2736                     @NotNull final StringBuilder buffer)
2737  {
2738    if (message != null)
2739    {
2740      if (buffer.length() > 0)
2741      {
2742        buffer.append("  ");
2743      }
2744
2745      buffer.append(message);
2746    }
2747  }
2748
2749
2750
2751  /**
2752   * {@inheritDoc}
2753   */
2754  @Override()
2755  public void handleUnsolicitedNotification(
2756                   @NotNull final LDAPConnection connection,
2757                   @NotNull final ExtendedResult notification)
2758  {
2759    wrapOut(0, 79,
2760         INFO_MOVE_SUBTREE_UNSOLICITED_NOTIFICATION.get(notification.getOID(),
2761              connection.getConnectionName(), notification.getResultCode(),
2762              notification.getDiagnosticMessage()));
2763  }
2764
2765
2766
2767  /**
2768   * {@inheritDoc}
2769   */
2770  @Override()
2771  @NotNull()
2772  public ReadOnlyEntry doPreAddProcessing(@NotNull final ReadOnlyEntry entry)
2773  {
2774    // No processing required.
2775    return entry;
2776  }
2777
2778
2779
2780  /**
2781   * {@inheritDoc}
2782   */
2783  @Override()
2784  public void doPostAddProcessing(@NotNull final ReadOnlyEntry entry)
2785  {
2786    wrapOut(0, 79, INFO_MOVE_SUBTREE_ADD_SUCCESSFUL.get(entry.getDN()));
2787  }
2788
2789
2790
2791  /**
2792   * {@inheritDoc}
2793   */
2794  @Override()
2795  public void doPreDeleteProcessing(@NotNull final DN entryDN)
2796  {
2797    // No processing required.
2798  }
2799
2800
2801
2802  /**
2803   * {@inheritDoc}
2804   */
2805  @Override()
2806  public void doPostDeleteProcessing(@NotNull final DN entryDN)
2807  {
2808    wrapOut(0, 79, INFO_MOVE_SUBTREE_DELETE_SUCCESSFUL.get(entryDN.toString()));
2809  }
2810
2811
2812
2813  /**
2814   * {@inheritDoc}
2815   */
2816  @Override()
2817  protected boolean registerShutdownHook()
2818  {
2819    return true;
2820  }
2821
2822
2823
2824  /**
2825   * {@inheritDoc}
2826   */
2827  @Override()
2828  protected void doShutdownHookProcessing(@Nullable final ResultCode resultCode)
2829  {
2830    if (resultCode != null)
2831    {
2832      // The tool exited normally, so we don't need to do anything.
2833      return;
2834    }
2835
2836    // If there is an interrupt message, then display it.
2837    wrapErr(0, 79, interruptMessage);
2838  }
2839
2840
2841
2842  /**
2843   * {@inheritDoc}
2844   */
2845  @Override()
2846  @NotNull()
2847  public LinkedHashMap<String[],String> getExampleUsages()
2848  {
2849    final LinkedHashMap<String[],String> exampleMap =
2850         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
2851
2852    final String[] args =
2853    {
2854      "--sourceHostname", "ds1.example.com",
2855      "--sourcePort", "389",
2856      "--sourceBindDN", "uid=admin,dc=example,dc=com",
2857      "--sourceBindPassword", "password",
2858      "--targetHostname", "ds2.example.com",
2859      "--targetPort", "389",
2860      "--targetBindDN", "uid=admin,dc=example,dc=com",
2861      "--targetBindPassword", "password",
2862      "--baseDN", "cn=small subtree,dc=example,dc=com",
2863      "--sizeLimit", "100",
2864      "--purpose", "Migrate a small subtree from ds1 to ds2"
2865    };
2866    exampleMap.put(args, INFO_MOVE_SUBTREE_EXAMPLE_DESCRIPTION.get());
2867
2868    return exampleMap;
2869  }
2870}