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 }