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