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