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