001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2016-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tools;
037
038
039
040import java.io.ByteArrayInputStream;
041import java.io.File;
042import java.io.InputStream;
043import java.io.IOException;
044import java.io.OutputStream;
045import java.math.BigDecimal;
046import java.util.ArrayList;
047import java.util.EnumSet;
048import java.util.HashSet;
049import java.util.LinkedHashMap;
050import java.util.List;
051import java.util.Map;
052import java.util.Set;
053import java.util.SortedMap;
054import java.util.StringTokenizer;
055import java.util.concurrent.TimeUnit;
056import java.util.concurrent.atomic.AtomicBoolean;
057
058import com.unboundid.asn1.ASN1OctetString;
059import com.unboundid.ldap.sdk.AddRequest;
060import com.unboundid.ldap.sdk.Control;
061import com.unboundid.ldap.sdk.DeleteRequest;
062import com.unboundid.ldap.sdk.DN;
063import com.unboundid.ldap.sdk.Entry;
064import com.unboundid.ldap.sdk.ExtendedResult;
065import com.unboundid.ldap.sdk.Filter;
066import com.unboundid.ldap.sdk.LDAPConnectionOptions;
067import com.unboundid.ldap.sdk.LDAPConnection;
068import com.unboundid.ldap.sdk.LDAPConnectionPool;
069import com.unboundid.ldap.sdk.LDAPException;
070import com.unboundid.ldap.sdk.LDAPRequest;
071import com.unboundid.ldap.sdk.LDAPResult;
072import com.unboundid.ldap.sdk.LDAPSearchException;
073import com.unboundid.ldap.sdk.Modification;
074import com.unboundid.ldap.sdk.ModifyRequest;
075import com.unboundid.ldap.sdk.ModifyDNRequest;
076import com.unboundid.ldap.sdk.ResultCode;
077import com.unboundid.ldap.sdk.SearchRequest;
078import com.unboundid.ldap.sdk.SearchResult;
079import com.unboundid.ldap.sdk.SearchScope;
080import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler;
081import com.unboundid.ldap.sdk.Version;
082import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
083import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
084import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
085import com.unboundid.ldap.sdk.controls.PermissiveModifyRequestControl;
086import com.unboundid.ldap.sdk.controls.PostReadRequestControl;
087import com.unboundid.ldap.sdk.controls.PreReadRequestControl;
088import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
089import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
090import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
091import com.unboundid.ldap.sdk.controls.SubtreeDeleteRequestControl;
092import com.unboundid.ldap.sdk.controls.TransactionSpecificationRequestControl;
093import com.unboundid.ldap.sdk.extensions.StartTransactionExtendedRequest;
094import com.unboundid.ldap.sdk.extensions.StartTransactionExtendedResult;
095import com.unboundid.ldap.sdk.extensions.EndTransactionExtendedRequest;
096import com.unboundid.ldap.sdk.unboundidds.controls.AccessLogFieldRequestControl;
097import com.unboundid.ldap.sdk.unboundidds.controls.AssuredReplicationLocalLevel;
098import com.unboundid.ldap.sdk.unboundidds.controls.
099            AssuredReplicationRequestControl;
100import com.unboundid.ldap.sdk.unboundidds.controls.
101            AssuredReplicationRemoteLevel;
102import com.unboundid.ldap.sdk.unboundidds.controls.
103            GenerateAccessTokenRequestControl;
104import com.unboundid.ldap.sdk.unboundidds.controls.
105            GeneratePasswordRequestControl;
106import com.unboundid.ldap.sdk.unboundidds.controls.
107            GetAuthorizationEntryRequestControl;
108import com.unboundid.ldap.sdk.unboundidds.controls.
109            GetBackendSetIDRequestControl;
110import com.unboundid.ldap.sdk.unboundidds.controls.
111            GetRecentLoginHistoryRequestControl;
112import com.unboundid.ldap.sdk.unboundidds.controls.
113            GetUserResourceLimitsRequestControl;
114import com.unboundid.ldap.sdk.unboundidds.controls.GetServerIDRequestControl;
115import com.unboundid.ldap.sdk.unboundidds.controls.HardDeleteRequestControl;
116import com.unboundid.ldap.sdk.unboundidds.controls.
117            IgnoreNoUserModificationRequestControl;
118import com.unboundid.ldap.sdk.unboundidds.controls.
119            JSONFormattedControlDecodeBehavior;
120import com.unboundid.ldap.sdk.unboundidds.controls.JSONFormattedRequestControl;
121import com.unboundid.ldap.sdk.unboundidds.controls.JSONFormattedResponseControl;
122import com.unboundid.ldap.sdk.unboundidds.controls.
123            NameWithEntryUUIDRequestControl;
124import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl;
125import com.unboundid.ldap.sdk.unboundidds.controls.
126            OperationPurposeRequestControl;
127import com.unboundid.ldap.sdk.unboundidds.controls.PasswordPolicyRequestControl;
128import com.unboundid.ldap.sdk.unboundidds.controls.
129            PasswordUpdateBehaviorRequestControl;
130import com.unboundid.ldap.sdk.unboundidds.controls.
131            PasswordUpdateBehaviorRequestControlProperties;
132import com.unboundid.ldap.sdk.unboundidds.controls.
133            PasswordValidationDetailsRequestControl;
134import com.unboundid.ldap.sdk.unboundidds.controls.PurgePasswordRequestControl;
135import com.unboundid.ldap.sdk.unboundidds.controls.
136            ReplicationRepairRequestControl;
137import com.unboundid.ldap.sdk.unboundidds.controls.RetirePasswordRequestControl;
138import com.unboundid.ldap.sdk.unboundidds.controls.
139            RouteToBackendSetRequestControl;
140import com.unboundid.ldap.sdk.unboundidds.controls.RouteToServerRequestControl;
141import com.unboundid.ldap.sdk.unboundidds.controls.SoftDeleteRequestControl;
142import com.unboundid.ldap.sdk.unboundidds.controls.
143            SuppressOperationalAttributeUpdateRequestControl;
144import com.unboundid.ldap.sdk.unboundidds.controls.
145            SuppressReferentialIntegrityUpdatesRequestControl;
146import com.unboundid.ldap.sdk.unboundidds.controls.
147            UniquenessMultipleAttributeBehavior;
148import com.unboundid.ldap.sdk.unboundidds.controls.UniquenessRequestControl;
149import com.unboundid.ldap.sdk.unboundidds.controls.
150            UniquenessRequestControlProperties;
151import com.unboundid.ldap.sdk.unboundidds.controls.SuppressType;
152import com.unboundid.ldap.sdk.unboundidds.controls.UndeleteRequestControl;
153import com.unboundid.ldap.sdk.unboundidds.controls.UniquenessValidationLevel;
154import com.unboundid.ldap.sdk.unboundidds.extensions.MultiUpdateErrorBehavior;
155import com.unboundid.ldap.sdk.unboundidds.extensions.MultiUpdateExtendedRequest;
156import com.unboundid.ldap.sdk.unboundidds.extensions.
157            StartAdministrativeSessionExtendedRequest;
158import com.unboundid.ldap.sdk.unboundidds.extensions.
159            StartAdministrativeSessionPostConnectProcessor;
160import com.unboundid.ldif.LDIFAddChangeRecord;
161import com.unboundid.ldif.LDIFChangeRecord;
162import com.unboundid.ldif.LDIFDeleteChangeRecord;
163import com.unboundid.ldif.LDIFException;
164import com.unboundid.ldif.LDIFModifyChangeRecord;
165import com.unboundid.ldif.LDIFModifyDNChangeRecord;
166import com.unboundid.ldif.LDIFReader;
167import com.unboundid.ldif.LDIFWriter;
168import com.unboundid.ldif.TrailingSpaceBehavior;
169import com.unboundid.util.Debug;
170import com.unboundid.util.DNFileReader;
171import com.unboundid.util.FilterFileReader;
172import com.unboundid.util.FixedRateBarrier;
173import com.unboundid.util.LDAPCommandLineTool;
174import com.unboundid.util.NotNull;
175import com.unboundid.util.Nullable;
176import com.unboundid.util.StaticUtils;
177import com.unboundid.util.SubtreeDeleter;
178import com.unboundid.util.SubtreeDeleterResult;
179import com.unboundid.util.ThreadSafety;
180import com.unboundid.util.ThreadSafetyLevel;
181import com.unboundid.util.args.ArgumentException;
182import com.unboundid.util.args.ArgumentParser;
183import com.unboundid.util.args.BooleanArgument;
184import com.unboundid.util.args.ControlArgument;
185import com.unboundid.util.args.DNArgument;
186import com.unboundid.util.args.DurationArgument;
187import com.unboundid.util.args.FileArgument;
188import com.unboundid.util.args.FilterArgument;
189import com.unboundid.util.args.IntegerArgument;
190import com.unboundid.util.args.StringArgument;
191import com.unboundid.util.json.JSONBoolean;
192import com.unboundid.util.json.JSONNumber;
193import com.unboundid.util.json.JSONObject;
194import com.unboundid.util.json.JSONString;
195import com.unboundid.util.json.JSONValue;
196
197import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
198
199
200
201/**
202 * This class provides an implementation of an LDAP command-line tool that may
203 * be used to apply changes to a directory server.  The changes to apply (which
204 * may include add, delete, modify, and modify DN operations) will be read in
205 * LDIF form, either from standard input or a specified file or set of files.
206 * This is a much more full-featured tool than the
207 * {@link com.unboundid.ldap.sdk.examples.LDAPModify} tool
208 * <BR>
209 * <BLOCKQUOTE>
210 *   <B>NOTE:</B>  This class, and other classes within the
211 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
212 *   supported for use against Ping Identity, UnboundID, and
213 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
214 *   for proprietary functionality or for external specifications that are not
215 *   considered stable or mature enough to be guaranteed to work in an
216 *   interoperable way with other types of LDAP servers.
217 * </BLOCKQUOTE>
218 */
219@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
220public final class LDAPModify
221       extends LDAPCommandLineTool
222       implements UnsolicitedNotificationHandler
223{
224  /**
225   * The column at which output should be wrapped.
226   */
227  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
228
229
230
231  /**
232   * The name of the attribute type used to specify a password in the
233   * authentication password syntax as described in RFC 3112.
234   */
235  @NotNull private static final String ATTR_AUTH_PASSWORD = "authPassword";
236
237
238
239  /**
240   * The name of the attribute type used to specify the DN of the soft-deleted
241   * entry to be restored via an undelete operation.
242   */
243  @NotNull private static final String ATTR_UNDELETE_FROM_DN =
244       "ds-undelete-from-dn";
245
246
247
248  /**
249   * The name of the attribute type used to specify a password in the
250   * userPassword syntax.
251   */
252  @NotNull private static final String ATTR_USER_PASSWORD = "userPassword";
253
254
255
256  /**
257   * The long identifier for the argument used to specify the desired assured
258   * replication local level.
259   */
260  @NotNull private static final String ARG_ASSURED_REPLICATION_LOCAL_LEVEL =
261       "assuredReplicationLocalLevel";
262
263
264
265  /**
266   * The long identifier for the argument used to specify the desired assured
267   * replication remote level.
268   */
269  @NotNull private static final String ARG_ASSURED_REPLICATION_REMOTE_LEVEL =
270       "assuredReplicationRemoteLevel";
271
272
273
274  /**
275   * The long identifier for the argument used to specify the desired assured
276   * timeout.
277   */
278  @NotNull private static final String ARG_ASSURED_REPLICATION_TIMEOUT =
279       "assuredReplicationTimeout";
280
281
282
283  /**
284   * The long identifier for the argument used to specify the path to an LDIF
285   * file containing changes to apply.
286   */
287  @NotNull private static final String ARG_LDIF_FILE = "ldifFile";
288
289
290
291  /**
292   * The long identifier for the argument used to specify the simple paged
293   * results page size to use when modifying entries that match a provided
294   * filter.
295   */
296  @NotNull private static final String ARG_SEARCH_PAGE_SIZE = "searchPageSize";
297
298
299
300  // The set of arguments supported by this program.
301  @Nullable private BooleanArgument allowUndelete = null;
302  @Nullable private BooleanArgument assuredReplication = null;
303  @Nullable private BooleanArgument authorizationIdentity = null;
304  @Nullable private BooleanArgument clientSideSubtreeDelete = null;
305  @Nullable private BooleanArgument continueOnError = null;
306  @Nullable private BooleanArgument defaultAdd = null;
307  @Nullable private BooleanArgument dryRun = null;
308  @Nullable private BooleanArgument followReferrals = null;
309  @Nullable private BooleanArgument generateAccessToken = null;
310  @Nullable private BooleanArgument generatePassword = null;
311  @Nullable private BooleanArgument getBackendSetID = null;
312  @Nullable private BooleanArgument getRecentLoginHistory = null;
313  @Nullable private BooleanArgument getServerID = null;
314  @Nullable private BooleanArgument getUserResourceLimits = null;
315  @Nullable private BooleanArgument hardDelete = null;
316  @Nullable private BooleanArgument ignoreNoUserModification = null;
317  @Nullable private BooleanArgument manageDsaIT = null;
318  @Nullable private BooleanArgument nameWithEntryUUID = null;
319  @Nullable private BooleanArgument neverRetry = null;
320  @Nullable private BooleanArgument noOperation = null;
321  @Nullable private BooleanArgument passwordValidationDetails = null;
322  @Nullable private BooleanArgument permissiveModify = null;
323  @Nullable private BooleanArgument purgeCurrentPassword = null;
324  @Nullable private BooleanArgument replicationRepair = null;
325  @Nullable private BooleanArgument retireCurrentPassword = null;
326  @Nullable private BooleanArgument retryFailedOperations = null;
327  @Nullable private BooleanArgument softDelete = null;
328  @Nullable private BooleanArgument stripTrailingSpaces = null;
329  @Nullable private BooleanArgument serverSideSubtreeDelete = null;
330  @Nullable private BooleanArgument suppressReferentialIntegrityUpdates = null;
331  @Nullable private BooleanArgument useAdministrativeSession = null;
332  @Nullable private BooleanArgument useJSONFormattedRequestControls = null;
333  @Nullable private BooleanArgument usePasswordPolicyControl = null;
334  @Nullable private BooleanArgument useTransaction = null;
335  @Nullable private BooleanArgument verbose = null;
336  @Nullable private ControlArgument addControl = null;
337  @Nullable private ControlArgument bindControl = null;
338  @Nullable private ControlArgument deleteControl = null;
339  @Nullable private ControlArgument modifyControl = null;
340  @Nullable private ControlArgument modifyDNControl = null;
341  @Nullable private ControlArgument operationControl = null;
342  @Nullable private DNArgument modifyEntryWithDN = null;
343  @Nullable private DNArgument proxyV1As = null;
344  @Nullable private DNArgument uniquenessBaseDN = null;
345  @Nullable private DurationArgument assuredReplicationTimeout = null;
346  @Nullable private FileArgument encryptionPassphraseFile = null;
347  @Nullable private FileArgument ldifFile = null;
348  @Nullable private FileArgument modifyEntriesMatchingFiltersFromFile = null;
349  @Nullable private FileArgument modifyEntriesWithDNsFromFile = null;
350  @Nullable private FileArgument rejectFile = null;
351  @Nullable private FilterArgument assertionFilter = null;
352  @Nullable private FilterArgument modifyEntriesMatchingFilter = null;
353  @Nullable private FilterArgument uniquenessFilter = null;
354  @Nullable private IntegerArgument ratePerSecond = null;
355  @Nullable private IntegerArgument searchPageSize = null;
356  @Nullable private StringArgument accessLogField = null;
357  @Nullable private StringArgument assuredReplicationLocalLevel = null;
358  @Nullable private StringArgument assuredReplicationRemoteLevel = null;
359  @Nullable private StringArgument characterSet = null;
360  @Nullable private StringArgument getAuthorizationEntryAttribute = null;
361  @Nullable private StringArgument multiUpdateErrorBehavior = null;
362  @Nullable private StringArgument operationPurpose = null;
363  @Nullable private StringArgument passwordUpdateBehavior = null;
364  @Nullable private StringArgument postReadAttribute = null;
365  @Nullable private StringArgument preReadAttribute = null;
366  @Nullable private StringArgument proxyAs = null;
367  @Nullable private StringArgument routeToBackendSet = null;
368  @Nullable private StringArgument routeToServer = null;
369  @Nullable private StringArgument suppressOperationalAttributeUpdates = null;
370  @Nullable private StringArgument uniquenessAttribute = null;
371  @Nullable private StringArgument uniquenessMultipleAttributeBehavior = null;
372  @Nullable private StringArgument uniquenessPostCommitValidationLevel = null;
373  @Nullable private StringArgument uniquenessPreCommitValidationLevel = null;
374
375  // Indicates whether we've written anything to the reject writer yet.
376  @NotNull private final AtomicBoolean rejectWritten;
377
378  // The input stream from to use for standard input.
379  @NotNull private final InputStream in;
380
381  // The route to backend set request controls to include in write requests.
382  @NotNull private final List<RouteToBackendSetRequestControl>
383       routeToBackendSetRequestControls = new ArrayList<>(10);
384
385
386
387  /**
388   * Runs this tool with the provided command-line arguments.  It will use the
389   * JVM-default streams for standard input, output, and error.
390   *
391   * @param  args  The command-line arguments to provide to this program.
392   */
393  public static void main(@NotNull final String... args)
394  {
395    final ResultCode resultCode = main(System.in, System.out, System.err, args);
396    if (resultCode != ResultCode.SUCCESS)
397    {
398      System.exit(Math.min(resultCode.intValue(), 255));
399    }
400  }
401
402
403
404  /**
405   * Runs this tool with the provided streams and command-line arguments.
406   *
407   * @param  in    The input stream to use for standard input.  If this is
408   *               {@code null}, then no standard input will be used.
409   * @param  out   The output stream to use for standard output.  If this is
410   *               {@code null}, then standard output will be suppressed.
411   * @param  err   The output stream to use for standard error.  If this is
412   *               {@code null}, then standard error will be suppressed.
413   * @param  args  The command-line arguments provided to this program.
414   *
415   * @return  The result code obtained when running the tool.  Any result code
416   *          other than {@link ResultCode#SUCCESS} indicates an error.
417   */
418  @NotNull()
419  public static ResultCode main(@Nullable final InputStream in,
420                                @Nullable final OutputStream out,
421                                @Nullable final OutputStream err,
422                                @NotNull final String... args)
423  {
424    final LDAPModify tool = new LDAPModify(in, out, err);
425    return tool.runTool(args);
426  }
427
428
429
430  /**
431   * Creates a new instance of this tool with the provided streams.  Standard
432   * input will not be available.
433   *
434   * @param  out  The output stream to use for standard output.  If this is
435   *              {@code null}, then standard output will be suppressed.
436   * @param  err  The output stream to use for standard error.  If this is
437   *              {@code null}, then standard error will be suppressed.
438   */
439  public LDAPModify(@Nullable final OutputStream out,
440                    @Nullable final OutputStream err)
441  {
442    this(null, out, err);
443  }
444
445
446
447  /**
448   * Creates a new instance of this tool with the provided streams.
449   *
450   * @param  in   The input stream to use for standard input.  If this is
451   *              {@code null}, then no standard input will be used.
452   * @param  out  The output stream to use for standard output.  If this is
453   *              {@code null}, then standard output will be suppressed.
454   * @param  err  The output stream to use for standard error.  If this is
455   *              {@code null}, then standard error will be suppressed.
456   */
457  public LDAPModify(@Nullable final InputStream in,
458                    @Nullable final OutputStream out,
459                    @Nullable final OutputStream err)
460  {
461    super(out, err);
462
463    if (in == null)
464    {
465      this.in = new ByteArrayInputStream(StaticUtils.NO_BYTES);
466    }
467    else
468    {
469      this.in = in;
470    }
471
472
473    rejectWritten = new AtomicBoolean(false);
474  }
475
476
477
478  /**
479   * {@inheritDoc}
480   */
481  @Override()
482  @NotNull()
483  public String getToolName()
484  {
485    return "ldapmodify";
486  }
487
488
489
490  /**
491   * {@inheritDoc}
492   */
493  @Override()
494  @NotNull()
495  public String getToolDescription()
496  {
497    return INFO_LDAPMODIFY_TOOL_DESCRIPTION.get(ARG_LDIF_FILE);
498  }
499
500
501
502  /**
503   * {@inheritDoc}
504   */
505  @Override()
506  @NotNull()
507  public String getToolVersion()
508  {
509    return Version.NUMERIC_VERSION_STRING;
510  }
511
512
513
514  /**
515   * {@inheritDoc}
516   */
517  @Override()
518  public boolean supportsInteractiveMode()
519  {
520    return true;
521  }
522
523
524
525  /**
526   * {@inheritDoc}
527   */
528  @Override()
529  public boolean defaultsToInteractiveMode()
530  {
531    return true;
532  }
533
534
535
536  /**
537   * {@inheritDoc}
538   */
539  @Override()
540  public boolean supportsPropertiesFile()
541  {
542    return true;
543  }
544
545
546
547  /**
548   * {@inheritDoc}
549   */
550  @Override()
551  public boolean supportsOutputFile()
552  {
553    return true;
554  }
555
556
557
558  /**
559   * {@inheritDoc}
560   */
561  @Override()
562  protected boolean defaultToPromptForBindPassword()
563  {
564    return true;
565  }
566
567
568
569  /**
570   * {@inheritDoc}
571   */
572  @Override()
573  protected boolean includeAlternateLongIdentifiers()
574  {
575    return true;
576  }
577
578
579
580  /**
581   * {@inheritDoc}
582   */
583  @Override()
584  protected boolean supportsSSLDebugging()
585  {
586    return true;
587  }
588
589
590
591  /**
592   * {@inheritDoc}
593   */
594  @Override()
595  protected boolean logToolInvocationByDefault()
596  {
597    return true;
598  }
599
600
601
602  /**
603   * {@inheritDoc}
604   */
605  @Override()
606  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
607         throws ArgumentException
608  {
609    ldifFile = new FileArgument('f', ARG_LDIF_FILE, false, -1, null,
610         INFO_LDAPMODIFY_ARG_DESCRIPTION_LDIF_FILE.get(), true, true, true,
611         false);
612    ldifFile.addLongIdentifier("filename", true);
613    ldifFile.addLongIdentifier("ldif-file", true);
614    ldifFile.addLongIdentifier("file-name", true);
615    ldifFile.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
616    parser.addArgument(ldifFile);
617
618
619    encryptionPassphraseFile = new FileArgument(null,
620         "encryptionPassphraseFile", false, 1, null,
621         INFO_LDAPMODIFY_ARG_DESCRIPTION_ENCRYPTION_PW_FILE.get(), true, true,
622         true, false);
623    encryptionPassphraseFile.addLongIdentifier("encryption-passphrase-file",
624         true);
625    encryptionPassphraseFile.addLongIdentifier("encryptionPasswordFile", true);
626    encryptionPassphraseFile.addLongIdentifier("encryption-password-file",
627         true);
628    encryptionPassphraseFile.setArgumentGroupName(
629         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
630    parser.addArgument(encryptionPassphraseFile);
631
632
633    characterSet = new StringArgument('i', "characterSet", false, 1,
634         INFO_LDAPMODIFY_PLACEHOLDER_CHARSET.get(),
635         INFO_LDAPMODIFY_ARG_DESCRIPTION_CHARACTER_SET.get(), "UTF-8");
636    characterSet.addLongIdentifier("encoding", true);
637    characterSet.addLongIdentifier("character-set", true);
638    characterSet.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
639    parser.addArgument(characterSet);
640
641
642    rejectFile = new FileArgument('R', "rejectFile", false, 1, null,
643         INFO_LDAPMODIFY_ARG_DESCRIPTION_REJECT_FILE.get(), false, true, true,
644         false);
645    rejectFile.addLongIdentifier("reject-file", true);
646    rejectFile.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
647    parser.addArgument(rejectFile);
648
649
650    verbose = new BooleanArgument('v', "verbose", 1,
651         INFO_LDAPMODIFY_ARG_DESCRIPTION_VERBOSE.get());
652    verbose.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
653    parser.addArgument(verbose);
654
655
656    modifyEntriesMatchingFilter = new FilterArgument(null,
657         "modifyEntriesMatchingFilter", false, 0, null,
658         INFO_LDAPMODIFY_ARG_DESCRIPTION_MODIFY_ENTRIES_MATCHING_FILTER.get(
659              ARG_SEARCH_PAGE_SIZE));
660    modifyEntriesMatchingFilter.addLongIdentifier(
661         "modify-entries-matching-filter", true);
662    modifyEntriesMatchingFilter.setArgumentGroupName(
663         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
664    parser.addArgument(modifyEntriesMatchingFilter);
665
666
667    modifyEntriesMatchingFiltersFromFile = new FileArgument(null,
668         "modifyEntriesMatchingFiltersFromFile", false, 0, null,
669         INFO_LDAPMODIFY_ARG_DESCRIPTION_MODIFY_FILTER_FILE.get(
670              ARG_SEARCH_PAGE_SIZE), true, false, true, false);
671    modifyEntriesMatchingFiltersFromFile.addLongIdentifier(
672         "modify-entries-matching-filters-from-file", true);
673    modifyEntriesMatchingFiltersFromFile.setArgumentGroupName(
674         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
675    parser.addArgument(modifyEntriesMatchingFiltersFromFile);
676
677
678    modifyEntryWithDN = new DNArgument(null, "modifyEntryWithDN", false, 0,
679         null, INFO_LDAPMODIFY_ARG_DESCRIPTION_MODIFY_ENTRY_DN.get());
680    modifyEntryWithDN.addLongIdentifier("modify-entry-with-dn", true);
681    modifyEntryWithDN.setArgumentGroupName(
682         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
683    parser.addArgument(modifyEntryWithDN);
684
685
686    modifyEntriesWithDNsFromFile = new FileArgument(null,
687         "modifyEntriesWithDNsFromFile", false, 0,
688         null, INFO_LDAPMODIFY_ARG_DESCRIPTION_MODIFY_DN_FILE.get(), true,
689         false, true, false);
690    modifyEntriesWithDNsFromFile.addLongIdentifier(
691         "modify-entries-with-dns-from-file", true);
692    modifyEntriesWithDNsFromFile.setArgumentGroupName(
693         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
694    parser.addArgument(modifyEntriesWithDNsFromFile);
695
696
697    searchPageSize = new IntegerArgument(null, ARG_SEARCH_PAGE_SIZE, false, 1,
698         null,
699         INFO_LDAPMODIFY_ARG_DESCRIPTION_SEARCH_PAGE_SIZE.get(
700              modifyEntriesMatchingFilter.getIdentifierString(),
701              modifyEntriesMatchingFiltersFromFile.getIdentifierString()),
702         1, Integer.MAX_VALUE);
703    searchPageSize.addLongIdentifier("search-page-size", true);
704    searchPageSize.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
705    parser.addArgument(searchPageSize);
706
707
708    // NOTE:  The retryFailedOperations argument is now hidden, as we will retry
709    // operations by default.  The neverRetry argument can be used to disable
710    // this.
711    retryFailedOperations = new BooleanArgument(null, "retryFailedOperations",
712         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_RETRY_FAILED_OPERATIONS.get());
713    retryFailedOperations.addLongIdentifier("retry-failed-operations", true);
714    retryFailedOperations.setArgumentGroupName(
715         INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
716    retryFailedOperations.setHidden(true);
717    parser.addArgument(retryFailedOperations);
718
719
720    neverRetry = new BooleanArgument(null, "neverRetry", 1,
721         INFO_LDAPMODIFY_ARG_DESC_NEVER_RETRY.get());
722    neverRetry.addLongIdentifier("never-retry", true);
723    neverRetry.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
724    parser.addArgument(neverRetry);
725
726
727    dryRun = new BooleanArgument('n', "dryRun", 1,
728         INFO_LDAPMODIFY_ARG_DESCRIPTION_DRY_RUN.get());
729    dryRun.addLongIdentifier("dry-run", true);
730    dryRun.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
731    parser.addArgument(dryRun);
732
733
734    defaultAdd = new BooleanArgument('a', "defaultAdd", 1,
735         INFO_LDAPMODIFY_ARG_DESCRIPTION_DEFAULT_ADD.get());
736    defaultAdd.addLongIdentifier("default-add", true);
737    defaultAdd.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
738    parser.addArgument(defaultAdd);
739
740
741    continueOnError = new BooleanArgument('c', "continueOnError", 1,
742         INFO_LDAPMODIFY_ARG_DESCRIPTION_CONTINUE_ON_ERROR.get());
743    continueOnError.addLongIdentifier("continue-on-error", true);
744    continueOnError.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
745    parser.addArgument(continueOnError);
746
747
748    stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1,
749         INFO_LDAPMODIFY_ARG_DESCRIPTION_STRIP_TRAILING_SPACES.get());
750    stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true);
751    stripTrailingSpaces.setArgumentGroupName(
752         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
753    parser.addArgument(stripTrailingSpaces);
754
755
756
757    followReferrals = new BooleanArgument(null, "followReferrals", 1,
758         INFO_LDAPMODIFY_ARG_DESCRIPTION_FOLLOW_REFERRALS.get());
759    followReferrals.addLongIdentifier("follow-referrals", true);
760    followReferrals.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
761    parser.addArgument(followReferrals);
762
763
764    proxyAs = new StringArgument('Y', "proxyAs", false, 1,
765         INFO_PLACEHOLDER_AUTHZID.get(),
766         INFO_LDAPMODIFY_ARG_DESCRIPTION_PROXY_AS.get());
767    proxyAs.addLongIdentifier("proxyV2As", true);
768    proxyAs.addLongIdentifier("proxy-as", true);
769    proxyAs.addLongIdentifier("proxy-v2-as", true);
770    proxyAs.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
771    parser.addArgument(proxyAs);
772
773    proxyV1As = new DNArgument(null, "proxyV1As", false, 1, null,
774         INFO_LDAPMODIFY_ARG_DESCRIPTION_PROXY_V1_AS.get());
775    proxyV1As.addLongIdentifier("proxy-v1-as", true);
776    proxyV1As.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
777    parser.addArgument(proxyV1As);
778
779
780    useAdministrativeSession = new BooleanArgument(null,
781         "useAdministrativeSession", 1,
782         INFO_LDAPMODIFY_ARG_DESCRIPTION_USE_ADMIN_SESSION.get());
783    useAdministrativeSession.addLongIdentifier("use-administrative-session",
784         true);
785    useAdministrativeSession.setArgumentGroupName(
786         INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
787    parser.addArgument(useAdministrativeSession);
788
789
790    accessLogField = new StringArgument(null, "accessLogField", false, 0,
791         INFO_LDAPMODIFY_ARG_PLACEHOLDER_NAME_VALUE.get(),
792         INFO_LDAMODIFY_ARG_DESCRIPTION_ACCESS_LOG_FIELD.get());
793    accessLogField.addLongIdentifier("access-log-field", true);
794    accessLogField.setArgumentGroupName(
795         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
796    parser.addArgument(accessLogField);
797
798
799    operationPurpose = new StringArgument(null, "operationPurpose", false, 1,
800         INFO_PLACEHOLDER_PURPOSE.get(),
801         INFO_LDAPMODIFY_ARG_DESCRIPTION_OPERATION_PURPOSE.get());
802    operationPurpose.addLongIdentifier("operation-purpose", true);
803    operationPurpose.setArgumentGroupName(
804         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
805    parser.addArgument(operationPurpose);
806
807
808    manageDsaIT = new BooleanArgument(null, "useManageDsaIT", 1,
809         INFO_LDAPMODIFY_ARG_DESCRIPTION_MANAGE_DSA_IT.get());
810    manageDsaIT.addLongIdentifier("manageDsaIT", true);
811    manageDsaIT.addLongIdentifier("use-manage-dsa-it", true);
812    manageDsaIT.addLongIdentifier("manage-dsa-it", true);
813    manageDsaIT.setArgumentGroupName(
814         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
815    parser.addArgument(manageDsaIT);
816
817
818    useTransaction = new BooleanArgument(null, "useTransaction", 1,
819         INFO_LDAPMODIFY_ARG_DESCRIPTION_USE_TRANSACTION.get());
820    useTransaction.addLongIdentifier("use-transaction", true);
821    useTransaction.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
822    parser.addArgument(useTransaction);
823
824
825    final Set<String> multiUpdateErrorBehaviorAllowedValues =
826         StaticUtils.setOf("atomic", "abort-on-error", "continue-on-error");
827    multiUpdateErrorBehavior = new StringArgument(null,
828         "multiUpdateErrorBehavior", false, 1,
829         "{atomic|abort-on-error|continue-on-error}",
830         INFO_LDAPMODIFY_ARG_DESCRIPTION_MULTI_UPDATE_ERROR_BEHAVIOR.get(),
831         multiUpdateErrorBehaviorAllowedValues);
832    multiUpdateErrorBehavior.addLongIdentifier("multi-update-error-behavior",
833         true);
834    multiUpdateErrorBehavior.setArgumentGroupName(
835         INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
836    parser.addArgument(multiUpdateErrorBehavior);
837
838
839    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
840         INFO_PLACEHOLDER_FILTER.get(),
841         INFO_LDAPMODIFY_ARG_DESCRIPTION_ASSERTION_FILTER.get());
842    assertionFilter.addLongIdentifier("assertion-filter", true);
843    assertionFilter.setArgumentGroupName(
844         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
845    parser.addArgument(assertionFilter);
846
847
848    authorizationIdentity = new BooleanArgument('E',
849         "authorizationIdentity", 1,
850         INFO_LDAPMODIFY_ARG_DESCRIPTION_AUTHZ_IDENTITY.get());
851    authorizationIdentity.addLongIdentifier("reportAuthzID", true);
852    authorizationIdentity.addLongIdentifier("authorization-identity", true);
853    authorizationIdentity.addLongIdentifier("report-authzID", true);
854    authorizationIdentity.addLongIdentifier("report-authz-id", true);
855    authorizationIdentity.setArgumentGroupName(
856         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
857    parser.addArgument(authorizationIdentity);
858
859
860    generateAccessToken = new BooleanArgument(null, "generateAccessToken", 1,
861         INFO_LDAPMODIFY_ARG_DESCRIPTION_GENERATE_ACCESS_TOKEN.get());
862    generateAccessToken.addLongIdentifier("generate-access-token", true);
863    generateAccessToken.addLongIdentifier("requestAccessToken", true);
864    generateAccessToken.addLongIdentifier("request-access-token", true);
865    generateAccessToken.setArgumentGroupName(
866         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
867    parser.addArgument(generateAccessToken);
868
869
870    generatePassword = new BooleanArgument(null, "generatePassword", 1,
871         INFO_LDAPMODIFY_ARG_DESCRIPTION_GENERATE_PASSWORD.get());
872    generatePassword.addLongIdentifier("generatePW", true);
873    generatePassword.addLongIdentifier("generate-password", true);
874    generatePassword.addLongIdentifier("generate-pw", true);
875    generatePassword.setArgumentGroupName(
876         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
877    parser.addArgument(generatePassword);
878
879
880    getAuthorizationEntryAttribute = new StringArgument(null,
881         "getAuthorizationEntryAttribute", false, 0,
882         INFO_PLACEHOLDER_ATTR.get(),
883         INFO_LDAPMODIFY_ARG_DESCRIPTION_GET_AUTHZ_ENTRY_ATTR.get());
884    getAuthorizationEntryAttribute.addLongIdentifier(
885         "get-authorization-entry-attribute", true);
886    getAuthorizationEntryAttribute.setArgumentGroupName(
887         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
888    parser.addArgument(getAuthorizationEntryAttribute);
889
890
891    getBackendSetID = new BooleanArgument(null, "getBackendSetID",
892         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_GET_BACKEND_SET_ID.get());
893    getBackendSetID.addLongIdentifier("get-backend-set-id", true);
894    getBackendSetID.setArgumentGroupName(
895         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
896    parser.addArgument(getBackendSetID);
897
898
899    getRecentLoginHistory = new BooleanArgument(null, "getRecentLoginHistory",
900         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_GET_RECENT_LOGIN_HISTORY.get());
901    getRecentLoginHistory.addLongIdentifier("get-recent-login-history", true);
902    getRecentLoginHistory.setArgumentGroupName(
903         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
904    parser.addArgument(getRecentLoginHistory);
905
906
907    getServerID = new BooleanArgument(null, "getServerID",
908         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_GET_SERVER_ID.get());
909    getServerID.addLongIdentifier("get-server-id", true);
910    getServerID.setArgumentGroupName(
911         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
912    parser.addArgument(getServerID);
913
914
915    getUserResourceLimits = new BooleanArgument(null, "getUserResourceLimits",
916         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_GET_USER_RESOURCE_LIMITS.get());
917    getUserResourceLimits.addLongIdentifier("get-user-resource-limits", true);
918    getUserResourceLimits.setArgumentGroupName(
919         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
920    parser.addArgument(getUserResourceLimits);
921
922
923    ignoreNoUserModification = new BooleanArgument(null,
924         "ignoreNoUserModification", 1,
925         INFO_LDAPMODIFY_ARG_DESCRIPTION_IGNORE_NO_USER_MOD.get());
926    ignoreNoUserModification.addLongIdentifier("ignore-no-user-modification",
927         true);
928    ignoreNoUserModification.setArgumentGroupName(
929         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
930    parser.addArgument(ignoreNoUserModification);
931
932
933    preReadAttribute = new StringArgument(null, "preReadAttribute", false, -1,
934         INFO_PLACEHOLDER_ATTR.get(),
935         INFO_LDAPMODIFY_ARG_DESCRIPTION_PRE_READ_ATTRIBUTE.get());
936    preReadAttribute.addLongIdentifier("preReadAttributes", true);
937    preReadAttribute.addLongIdentifier("pre-read-attribute", true);
938    preReadAttribute.addLongIdentifier("pre-read-attributes", true);
939    preReadAttribute.setArgumentGroupName(
940         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
941    parser.addArgument(preReadAttribute);
942
943
944    postReadAttribute = new StringArgument(null, "postReadAttribute", false,
945         -1, INFO_PLACEHOLDER_ATTR.get(),
946         INFO_LDAPMODIFY_ARG_DESCRIPTION_POST_READ_ATTRIBUTE.get());
947    postReadAttribute.addLongIdentifier("postReadAttributes", true);
948    postReadAttribute.addLongIdentifier("post-read-attribute", true);
949    postReadAttribute.addLongIdentifier("post-read-attributes", true);
950    postReadAttribute.setArgumentGroupName(
951         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
952    parser.addArgument(postReadAttribute);
953
954
955    routeToBackendSet = new StringArgument(null, "routeToBackendSet",
956         false, 0,
957         INFO_LDAPMODIFY_ARG_PLACEHOLDER_ROUTE_TO_BACKEND_SET.get(),
958         INFO_LDAPMODIFY_ARG_DESCRIPTION_ROUTE_TO_BACKEND_SET.get());
959    routeToBackendSet.addLongIdentifier("route-to-backend-set", true);
960    routeToBackendSet.setArgumentGroupName(
961         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
962    parser.addArgument(routeToBackendSet);
963
964
965    routeToServer = new StringArgument(null, "routeToServer", false, 1,
966         INFO_LDAPMODIFY_ARG_PLACEHOLDER_ROUTE_TO_SERVER.get(),
967         INFO_LDAPMODIFY_ARG_DESCRIPTION_ROUTE_TO_SERVER.get());
968    routeToServer.addLongIdentifier("route-to-server", true);
969    routeToServer.setArgumentGroupName(
970         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
971    parser.addArgument(routeToServer);
972
973
974    assuredReplication = new BooleanArgument(null, "useAssuredReplication", 1,
975         INFO_LDAPMODIFY_ARG_DESCRIPTION_ASSURED_REPLICATION.get(
976              ARG_ASSURED_REPLICATION_LOCAL_LEVEL,
977              ARG_ASSURED_REPLICATION_REMOTE_LEVEL,
978              ARG_ASSURED_REPLICATION_TIMEOUT));
979    assuredReplication.addLongIdentifier("assuredReplication", true);
980    assuredReplication.addLongIdentifier("use-assured-replication", true);
981    assuredReplication.addLongIdentifier("assured-replication", true);
982    assuredReplication.setArgumentGroupName(
983         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
984    parser.addArgument(assuredReplication);
985
986
987    final Set<String> assuredReplicationLocalLevelAllowedValues =
988         StaticUtils.setOf("none", "received-any-server",
989              "processed-all-servers");
990    assuredReplicationLocalLevel = new StringArgument(null,
991         ARG_ASSURED_REPLICATION_LOCAL_LEVEL, false, 1,
992         INFO_PLACEHOLDER_LEVEL.get(),
993         INFO_LDAPMODIFY_ARG_DESCRIPTION_ASSURED_REPL_LOCAL_LEVEL.get(
994              assuredReplication.getIdentifierString()),
995         assuredReplicationLocalLevelAllowedValues);
996    assuredReplicationLocalLevel.addLongIdentifier(
997         "assured-replication-local-level", true);
998    assuredReplicationLocalLevel.setArgumentGroupName(
999         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1000    parser.addArgument(assuredReplicationLocalLevel);
1001
1002
1003    final Set<String> assuredReplicationRemoteLevelAllowedValues =
1004         StaticUtils.setOf("none", "received-any-remote-location",
1005              "received-all-remote-locations", "processed-all-remote-servers");
1006    assuredReplicationRemoteLevel = new StringArgument(null,
1007         ARG_ASSURED_REPLICATION_REMOTE_LEVEL, false, 1,
1008         INFO_PLACEHOLDER_LEVEL.get(),
1009         INFO_LDAPMODIFY_ARG_DESCRIPTION_ASSURED_REPL_REMOTE_LEVEL.get(
1010              assuredReplication.getIdentifierString()),
1011         assuredReplicationRemoteLevelAllowedValues);
1012    assuredReplicationRemoteLevel.addLongIdentifier(
1013         "assured-replication-remote-level", true);
1014    assuredReplicationRemoteLevel.setArgumentGroupName(
1015         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1016    parser.addArgument(assuredReplicationRemoteLevel);
1017
1018
1019    assuredReplicationTimeout = new DurationArgument(null,
1020         ARG_ASSURED_REPLICATION_TIMEOUT, false, INFO_PLACEHOLDER_TIMEOUT.get(),
1021         INFO_LDAPMODIFY_ARG_DESCRIPTION_ASSURED_REPL_TIMEOUT.get(
1022              assuredReplication.getIdentifierString()));
1023    assuredReplicationTimeout.setArgumentGroupName(
1024         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1025    parser.addArgument(assuredReplicationTimeout);
1026
1027
1028    replicationRepair = new BooleanArgument(null, "replicationRepair",
1029         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_REPLICATION_REPAIR.get());
1030    replicationRepair.addLongIdentifier("replication-repair", true);
1031    replicationRepair.setArgumentGroupName(
1032         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1033    parser.addArgument(replicationRepair);
1034
1035
1036    nameWithEntryUUID = new BooleanArgument(null, "nameWithEntryUUID", 1,
1037         INFO_LDAPMODIFY_ARG_DESCRIPTION_NAME_WITH_ENTRY_UUID.get());
1038    nameWithEntryUUID.addLongIdentifier("name-with-entryUUID", true);
1039    nameWithEntryUUID.addLongIdentifier("name-with-entry-uuid", true);
1040    nameWithEntryUUID.setArgumentGroupName(
1041         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1042    parser.addArgument(nameWithEntryUUID);
1043
1044
1045    noOperation = new BooleanArgument(null, "noOperation", 1,
1046         INFO_LDAPMODIFY_ARG_DESCRIPTION_NO_OPERATION.get());
1047    noOperation.addLongIdentifier("noOp", true);
1048    noOperation.addLongIdentifier("no-operation", true);
1049    noOperation.addLongIdentifier("no-op", true);
1050    noOperation.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1051    parser.addArgument(noOperation);
1052
1053
1054    passwordUpdateBehavior = new StringArgument(null,
1055         "passwordUpdateBehavior", false, 0,
1056         INFO_LDAPMODIFY_PLACEHOLDER_NAME_EQUALS_VALUE.get(),
1057         INFO_LDAPMODIFY_ARG_DESCRIPTION_PW_UPDATE_BEHAVIOR.get());
1058    passwordUpdateBehavior.addLongIdentifier("password-update-behavior", true);
1059    passwordUpdateBehavior.setArgumentGroupName(
1060         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1061    parser.addArgument(passwordUpdateBehavior);
1062
1063    passwordValidationDetails = new BooleanArgument(null,
1064         "getPasswordValidationDetails", 1,
1065         INFO_LDAPMODIFY_ARG_DESCRIPTION_PASSWORD_VALIDATION_DETAILS.get(
1066              ATTR_USER_PASSWORD, ATTR_AUTH_PASSWORD));
1067    passwordValidationDetails.addLongIdentifier("passwordValidationDetails",
1068         true);
1069    passwordValidationDetails.addLongIdentifier(
1070         "get-password-validation-details", true);
1071    passwordValidationDetails.addLongIdentifier("password-validation-details",
1072         true);
1073    passwordValidationDetails.setArgumentGroupName(
1074         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1075    parser.addArgument(passwordValidationDetails);
1076
1077
1078    permissiveModify = new BooleanArgument(null, "permissiveModify",
1079         1, INFO_LDAPMODIFY_ARG_DESCRIPTION_PERMISSIVE_MODIFY.get());
1080    permissiveModify.addLongIdentifier("permissive-modify", true);
1081    permissiveModify.setArgumentGroupName(
1082         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1083    parser.addArgument(permissiveModify);
1084
1085
1086    clientSideSubtreeDelete = new BooleanArgument(null,
1087         "clientSideSubtreeDelete", 1,
1088         INFO_LDAPMODIFY_ARG_DESCRIPTION_CLIENT_SIDE_SUBTREE_DELETE.get());
1089    clientSideSubtreeDelete.addLongIdentifier("client-side-subtree-delete",
1090         true);
1091    clientSideSubtreeDelete.setArgumentGroupName(
1092         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1093    parser.addArgument(clientSideSubtreeDelete);
1094
1095
1096    serverSideSubtreeDelete = new BooleanArgument(null,
1097         "serverSideSubtreeDelete", 1,
1098         INFO_LDAPMODIFY_ARG_DESCRIPTION_SERVER_SIDE_SUBTREE_DELETE.get());
1099    serverSideSubtreeDelete.addLongIdentifier("server-side-subtree-delete",
1100         true);
1101    serverSideSubtreeDelete.addLongIdentifier("subtreeDelete", true);
1102    serverSideSubtreeDelete.addLongIdentifier("subtree-delete", true);
1103    serverSideSubtreeDelete.addLongIdentifier("subtreeDeleteControl", true);
1104    serverSideSubtreeDelete.addLongIdentifier("subtree-delete-control", true);
1105    serverSideSubtreeDelete.addLongIdentifier("useSubtreeDeleteControl", true);
1106    serverSideSubtreeDelete.addLongIdentifier("use-subtree-delete-control",
1107         true);
1108    serverSideSubtreeDelete.setArgumentGroupName(
1109         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1110    parser.addArgument(serverSideSubtreeDelete);
1111
1112
1113    softDelete = new BooleanArgument('s', "softDelete", 1,
1114         INFO_LDAPMODIFY_ARG_DESCRIPTION_SOFT_DELETE.get());
1115    softDelete.addLongIdentifier("useSoftDelete", true);
1116    softDelete.addLongIdentifier("soft-delete", true);
1117    softDelete.addLongIdentifier("use-soft-delete", true);
1118    softDelete.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1119    parser.addArgument(softDelete);
1120
1121
1122    hardDelete = new BooleanArgument(null, "hardDelete", 1,
1123         INFO_LDAPMODIFY_ARG_DESCRIPTION_HARD_DELETE.get());
1124    hardDelete.addLongIdentifier("hard-delete", true);
1125    hardDelete.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1126    parser.addArgument(hardDelete);
1127
1128
1129    allowUndelete = new BooleanArgument(null, "allowUndelete", 1,
1130         INFO_LDAPMODIFY_ARG_DESCRIPTION_ALLOW_UNDELETE.get(
1131              ATTR_UNDELETE_FROM_DN));
1132    allowUndelete.addLongIdentifier("allow-undelete", true);
1133    allowUndelete.setArgumentGroupName(
1134         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1135    parser.addArgument(allowUndelete);
1136
1137
1138    retireCurrentPassword = new BooleanArgument(null, "retireCurrentPassword",
1139         1,
1140         INFO_LDAPMODIFY_ARG_DESCRIPTION_RETIRE_CURRENT_PASSWORD.get(
1141              ATTR_USER_PASSWORD, ATTR_AUTH_PASSWORD));
1142    retireCurrentPassword.addLongIdentifier("retire-current-password", true);
1143    retireCurrentPassword.setArgumentGroupName(
1144         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1145    parser.addArgument(retireCurrentPassword);
1146
1147
1148    purgeCurrentPassword = new BooleanArgument(null, "purgeCurrentPassword", 1,
1149         INFO_LDAPMODIFY_ARG_DESCRIPTION_PURGE_CURRENT_PASSWORD.get(
1150              ATTR_USER_PASSWORD, ATTR_AUTH_PASSWORD));
1151    purgeCurrentPassword.addLongIdentifier("purge-current-password", true);
1152    purgeCurrentPassword.setArgumentGroupName(
1153         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1154    parser.addArgument(purgeCurrentPassword);
1155
1156
1157    final Set<String> suppressOperationalAttributeUpdatesAllowedValues =
1158         StaticUtils.setOf("last-access-time", "last-login-time",
1159              "last-login-ip", "lastmod");
1160    suppressOperationalAttributeUpdates = new StringArgument(null,
1161         "suppressOperationalAttributeUpdates", false, -1,
1162         INFO_PLACEHOLDER_ATTR.get(),
1163         INFO_LDAPMODIFY_ARG_DESCRIPTION_SUPPRESS_OP_ATTR_UPDATES.get(),
1164         suppressOperationalAttributeUpdatesAllowedValues);
1165    suppressOperationalAttributeUpdates.addLongIdentifier(
1166         "suppress-operational-attribute-updates", true);
1167    suppressOperationalAttributeUpdates.setArgumentGroupName(
1168         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1169    parser.addArgument(suppressOperationalAttributeUpdates);
1170
1171
1172    suppressReferentialIntegrityUpdates = new BooleanArgument(null,
1173         "suppressReferentialIntegrityUpdates", 1,
1174         INFO_LDAPMODIFY_ARG_DESCRIPTION_SUPPRESS_REFERINT_UPDATES.get());
1175    suppressReferentialIntegrityUpdates.addLongIdentifier(
1176         "suppress-referential-integrity-updates", true);
1177    suppressReferentialIntegrityUpdates.setArgumentGroupName(
1178         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1179    parser.addArgument(suppressReferentialIntegrityUpdates);
1180
1181
1182    usePasswordPolicyControl = new BooleanArgument(null,
1183         "usePasswordPolicyControl", 1,
1184         INFO_LDAPMODIFY_ARG_DESCRIPTION_PASSWORD_POLICY.get());
1185    usePasswordPolicyControl.addLongIdentifier("use-password-policy-control",
1186         true);
1187    usePasswordPolicyControl.setArgumentGroupName(
1188         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1189    parser.addArgument(usePasswordPolicyControl);
1190
1191
1192    uniquenessAttribute = new StringArgument(null, "uniquenessAttribute", false,
1193         0, INFO_PLACEHOLDER_ATTR.get(),
1194        INFO_LDAPMODIFY_ARG_DESCRIPTION_UNIQUE_ATTR.get());
1195    uniquenessAttribute.addLongIdentifier("uniquenessAttributeType", true);
1196    uniquenessAttribute.addLongIdentifier("uniqueAttribute", true);
1197    uniquenessAttribute.addLongIdentifier("uniqueAttributeType", true);
1198    uniquenessAttribute.addLongIdentifier("uniqueness-attribute", true);
1199    uniquenessAttribute.addLongIdentifier("uniqueness-attribute-type", true);
1200    uniquenessAttribute.addLongIdentifier("unique-attribute", true);
1201    uniquenessAttribute.addLongIdentifier("unique-attribute-type", true);
1202    uniquenessAttribute.setArgumentGroupName(
1203         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1204    parser.addArgument(uniquenessAttribute);
1205
1206
1207    uniquenessFilter = new FilterArgument(null, "uniquenessFilter", false, 1,
1208         null, INFO_LDAPMODIFY_ARG_DESCRIPTION_UNIQUE_FILTER.get());
1209    uniquenessFilter.addLongIdentifier("uniqueness-filter", true);
1210    uniquenessFilter.setArgumentGroupName(
1211         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1212    parser.addArgument(uniquenessFilter);
1213
1214
1215    uniquenessBaseDN = new DNArgument(null, "uniquenessBaseDN", false, 1, null,
1216         INFO_LDAPMODIFY_ARG_DESCRIPTION_UNIQUE_BASE_DN.get());
1217    uniquenessBaseDN.addLongIdentifier("uniqueness-base-dn", true);
1218    uniquenessBaseDN.setArgumentGroupName(
1219         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1220    parser.addArgument(uniquenessBaseDN);
1221    parser.addDependentArgumentSet(uniquenessBaseDN, uniquenessAttribute,
1222         uniquenessFilter);
1223
1224
1225    final Set<String> mabValues = StaticUtils.setOf(
1226         "unique-within-each-attribute",
1227         "unique-across-all-attributes-including-in-same-entry",
1228         "unique-across-all-attributes-except-in-same-entry",
1229         "unique-in-combination");
1230    uniquenessMultipleAttributeBehavior = new StringArgument(null,
1231         "uniquenessMultipleAttributeBehavior", false, 1,
1232         INFO_LDAPMODIFY_PLACEHOLDER_BEHAVIOR.get(),
1233         INFO_LDAPMODIFY_ARG_DESCRIPTION_UNIQUE_MULTIPLE_ATTRIBUTE_BEHAVIOR.
1234              get(),
1235         mabValues);
1236    uniquenessMultipleAttributeBehavior.addLongIdentifier(
1237         "uniqueness-multiple-attribute-behavior", true);
1238    uniquenessMultipleAttributeBehavior.setArgumentGroupName(
1239         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1240    parser.addArgument(uniquenessMultipleAttributeBehavior);
1241    parser.addDependentArgumentSet(uniquenessMultipleAttributeBehavior,
1242         uniquenessAttribute);
1243
1244
1245    final Set<String> vlValues = StaticUtils.setOf("none", "all-subtree-views",
1246         "all-backend-sets", "all-available-backend-servers");
1247    uniquenessPreCommitValidationLevel = new StringArgument(null,
1248         "uniquenessPreCommitValidationLevel", false, 1,
1249         INFO_LDAPMODIFY_PLACEHOLDER_LEVEL.get(),
1250         INFO_LDAPMODIFY_ARG_DESCRIPTION_UNIQUE_PRE_COMMIT_LEVEL.get(),
1251         vlValues);
1252    uniquenessPreCommitValidationLevel.addLongIdentifier(
1253         "uniqueness-pre-commit-validation-level", true);
1254    uniquenessPreCommitValidationLevel.setArgumentGroupName(
1255         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1256    parser.addArgument(uniquenessPreCommitValidationLevel);
1257    parser.addDependentArgumentSet(uniquenessPreCommitValidationLevel,
1258         uniquenessAttribute, uniquenessFilter);
1259
1260
1261    uniquenessPostCommitValidationLevel = new StringArgument(null,
1262         "uniquenessPostCommitValidationLevel", false, 1,
1263         INFO_LDAPMODIFY_PLACEHOLDER_LEVEL.get(),
1264         INFO_LDAPMODIFY_ARG_DESCRIPTION_UNIQUE_POST_COMMIT_LEVEL.get(),
1265         vlValues);
1266    uniquenessPostCommitValidationLevel.addLongIdentifier(
1267         "uniqueness-post-commit-validation-level", true);
1268    uniquenessPostCommitValidationLevel.setArgumentGroupName(
1269         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1270    parser.addArgument(uniquenessPostCommitValidationLevel);
1271    parser.addDependentArgumentSet(uniquenessPostCommitValidationLevel,
1272         uniquenessAttribute, uniquenessFilter);
1273
1274    operationControl = new ControlArgument('J', "control", false, 0, null,
1275         INFO_LDAPMODIFY_ARG_DESCRIPTION_OP_CONTROL.get());
1276    operationControl.setArgumentGroupName(
1277         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1278    parser.addArgument(operationControl);
1279
1280
1281    addControl = new ControlArgument(null, "addControl", false, 0, null,
1282         INFO_LDAPMODIFY_ARG_DESCRIPTION_ADD_CONTROL.get());
1283    addControl.addLongIdentifier("add-control", true);
1284    addControl.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1285    parser.addArgument(addControl);
1286
1287
1288    bindControl = new ControlArgument(null, "bindControl", false, 0, null,
1289         INFO_LDAPMODIFY_ARG_DESCRIPTION_BIND_CONTROL.get());
1290    bindControl.addLongIdentifier("bind-control", true);
1291    bindControl.setArgumentGroupName(
1292         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1293    parser.addArgument(bindControl);
1294
1295
1296    deleteControl = new ControlArgument(null, "deleteControl", false, 0, null,
1297         INFO_LDAPMODIFY_ARG_DESCRIPTION_DELETE_CONTROL.get());
1298    deleteControl.addLongIdentifier("delete-control", true);
1299    deleteControl.setArgumentGroupName(
1300         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1301    parser.addArgument(deleteControl);
1302
1303
1304    modifyControl = new ControlArgument(null, "modifyControl", false, 0, null,
1305         INFO_LDAPMODIFY_ARG_DESCRIPTION_MODIFY_CONTROL.get());
1306    modifyControl.addLongIdentifier("modify-control", true);
1307    modifyControl.setArgumentGroupName(
1308         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1309    parser.addArgument(modifyControl);
1310
1311
1312    modifyDNControl = new ControlArgument(null, "modifyDNControl", false, 0,
1313         null, INFO_LDAPMODIFY_ARG_DESCRIPTION_MODIFY_DN_CONTROL.get());
1314    modifyDNControl.addLongIdentifier("modify-dn-control", true);
1315    modifyDNControl.setArgumentGroupName(
1316         INFO_LDAPMODIFY_ARG_GROUP_CONTROLS.get());
1317    parser.addArgument(modifyDNControl);
1318
1319
1320    useJSONFormattedRequestControls = new BooleanArgument(null,
1321         "useJSONFormattedRequestControls", 1,
1322         INFO_LDAPMODIFY_ARG_DESCRIPTION_USE_JSON_FORMATTED_CONTROLS.get());
1323    useJSONFormattedRequestControls.addLongIdentifier(
1324         "use-json-formatted-request-controls", true);
1325    useJSONFormattedRequestControls.addLongIdentifier(
1326         "useJSONFormattedControls", true);
1327    useJSONFormattedRequestControls.addLongIdentifier(
1328         "use-json-formatted-controls", true);
1329    useJSONFormattedRequestControls.setArgumentGroupName(
1330         INFO_LDAPSEARCH_ARG_GROUP_CONTROLS.get());
1331    parser.addArgument(useJSONFormattedRequestControls);
1332
1333
1334    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
1335         INFO_PLACEHOLDER_NUM.get(),
1336         INFO_LDAPMODIFY_ARG_DESCRIPTION_RATE_PER_SECOND.get(), 1,
1337         Integer.MAX_VALUE);
1338    ratePerSecond.addLongIdentifier("rate-per-second", true);
1339    ratePerSecond.setArgumentGroupName(INFO_LDAPMODIFY_ARG_GROUP_OPS.get());
1340    parser.addArgument(ratePerSecond);
1341
1342
1343    // The "--scriptFriendly" argument is provided for compatibility with legacy
1344    // ldapmodify tools, but is not actually used by this tool.
1345    final BooleanArgument scriptFriendly = new BooleanArgument(null,
1346         "scriptFriendly", 1,
1347         INFO_LDAPMODIFY_ARG_DESCRIPTION_SCRIPT_FRIENDLY.get());
1348    scriptFriendly.addLongIdentifier("script-friendly", true);
1349    scriptFriendly.setArgumentGroupName(
1350         INFO_LDAPMODIFY_ARG_GROUP_DATA.get());
1351    scriptFriendly.setHidden(true);
1352    parser.addArgument(scriptFriendly);
1353
1354
1355    // The "-V" / "--ldapVersion" argument is provided for compatibility with
1356    // legacy ldapmodify tools, but is not actually used by this tool.
1357    final IntegerArgument ldapVersion = new IntegerArgument('V', "ldapVersion",
1358         false, 1, null, INFO_LDAPMODIFY_ARG_DESCRIPTION_LDAP_VERSION.get());
1359    ldapVersion.addLongIdentifier("ldap-version", true);
1360    ldapVersion.setHidden(true);
1361    parser.addArgument(ldapVersion);
1362
1363
1364    // A few assured replication arguments will only be allowed if assured
1365    // replication is to be used.
1366    parser.addDependentArgumentSet(assuredReplicationLocalLevel,
1367         assuredReplication);
1368    parser.addDependentArgumentSet(assuredReplicationRemoteLevel,
1369         assuredReplication);
1370    parser.addDependentArgumentSet(assuredReplicationTimeout,
1371         assuredReplication);
1372
1373    // Transactions will be incompatible with a lot of settings.
1374    parser.addExclusiveArgumentSet(useTransaction, multiUpdateErrorBehavior);
1375    parser.addExclusiveArgumentSet(useTransaction, rejectFile);
1376    parser.addExclusiveArgumentSet(useTransaction, retryFailedOperations);
1377    parser.addExclusiveArgumentSet(useTransaction, continueOnError);
1378    parser.addExclusiveArgumentSet(useTransaction, dryRun);
1379    parser.addExclusiveArgumentSet(useTransaction, followReferrals);
1380    parser.addExclusiveArgumentSet(useTransaction, nameWithEntryUUID);
1381    parser.addExclusiveArgumentSet(useTransaction, noOperation);
1382    parser.addExclusiveArgumentSet(useTransaction, modifyEntriesMatchingFilter);
1383    parser.addExclusiveArgumentSet(useTransaction,
1384         modifyEntriesMatchingFiltersFromFile);
1385    parser.addExclusiveArgumentSet(useTransaction, modifyEntryWithDN);
1386    parser.addExclusiveArgumentSet(useTransaction,
1387         modifyEntriesWithDNsFromFile);
1388    parser.addExclusiveArgumentSet(useTransaction,
1389         clientSideSubtreeDelete);
1390
1391    // Multi-update is incompatible with a lot of settings.
1392    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, ratePerSecond);
1393    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, rejectFile);
1394    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior,
1395         retryFailedOperations);
1396    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, continueOnError);
1397    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, dryRun);
1398    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, followReferrals);
1399    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, nameWithEntryUUID);
1400    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, noOperation);
1401    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior,
1402         modifyEntriesMatchingFilter);
1403    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior,
1404         modifyEntriesMatchingFiltersFromFile);
1405    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior, modifyEntryWithDN);
1406    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior,
1407         modifyEntriesWithDNsFromFile);
1408    parser.addExclusiveArgumentSet(multiUpdateErrorBehavior,
1409         clientSideSubtreeDelete);
1410
1411    // Client-side and server-side subtree deletes cannot be used together.
1412    parser.addExclusiveArgumentSet(clientSideSubtreeDelete,
1413         serverSideSubtreeDelete);
1414
1415    // Soft delete cannot be used with either hard delete or subtree delete.
1416    parser.addExclusiveArgumentSet(softDelete, hardDelete);
1417    parser.addExclusiveArgumentSet(softDelete, clientSideSubtreeDelete);
1418    parser.addExclusiveArgumentSet(softDelete, serverSideSubtreeDelete);
1419
1420    // Client-side subtree delete cannot be used in conjunction with a few
1421    // other settings.
1422    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, followReferrals);
1423    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, preReadAttribute);
1424    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, getBackendSetID);
1425    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, getServerID);
1426    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, noOperation);
1427    parser.addExclusiveArgumentSet(clientSideSubtreeDelete, dryRun);
1428
1429    // Password retiring and purging can't be used together.
1430    parser.addExclusiveArgumentSet(retireCurrentPassword, purgeCurrentPassword);
1431
1432    // Referral following cannot be used in conjunction with the manageDsaIT
1433    // control.
1434    parser.addExclusiveArgumentSet(followReferrals, manageDsaIT);
1435
1436    // The proxyAs and proxyV1As arguments cannot be used together.
1437    parser.addExclusiveArgumentSet(proxyAs, proxyV1As);
1438
1439    // The modifyEntriesMatchingFilter argument is incompatible with a lot of
1440    // settings, since it can only be used for modify operations.
1441    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, allowUndelete);
1442    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, defaultAdd);
1443    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, dryRun);
1444    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, hardDelete);
1445    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter,
1446         ignoreNoUserModification);
1447    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter,
1448         nameWithEntryUUID);
1449    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, softDelete);
1450    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter,
1451         clientSideSubtreeDelete);
1452    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter,
1453         serverSideSubtreeDelete);
1454    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter,
1455         suppressReferentialIntegrityUpdates);
1456    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, addControl);
1457    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter, deleteControl);
1458    parser.addExclusiveArgumentSet(modifyEntriesMatchingFilter,
1459         modifyDNControl);
1460
1461    // The modifyEntriesMatchingFilterFromFile argument is incompatible with a
1462    // lot of settings, since it can only be used for modify operations.
1463    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1464         allowUndelete);
1465    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1466         defaultAdd);
1467    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1468         dryRun);
1469    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1470         hardDelete);
1471    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1472         ignoreNoUserModification);
1473    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1474         nameWithEntryUUID);
1475    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1476         softDelete);
1477    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1478         clientSideSubtreeDelete);
1479    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1480         serverSideSubtreeDelete);
1481    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1482         suppressReferentialIntegrityUpdates);
1483    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1484         addControl);
1485    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1486         deleteControl);
1487    parser.addExclusiveArgumentSet(modifyEntriesMatchingFiltersFromFile,
1488         modifyDNControl);
1489
1490    // The modifyEntryWithDN argument is incompatible with a lot of
1491    // settings, since it can only be used for modify operations.
1492    parser.addExclusiveArgumentSet(modifyEntryWithDN, allowUndelete);
1493    parser.addExclusiveArgumentSet(modifyEntryWithDN, defaultAdd);
1494    parser.addExclusiveArgumentSet(modifyEntryWithDN, dryRun);
1495    parser.addExclusiveArgumentSet(modifyEntryWithDN, hardDelete);
1496    parser.addExclusiveArgumentSet(modifyEntryWithDN, ignoreNoUserModification);
1497    parser.addExclusiveArgumentSet(modifyEntryWithDN, nameWithEntryUUID);
1498    parser.addExclusiveArgumentSet(modifyEntryWithDN, softDelete);
1499    parser.addExclusiveArgumentSet(modifyEntryWithDN, clientSideSubtreeDelete);
1500    parser.addExclusiveArgumentSet(modifyEntryWithDN, serverSideSubtreeDelete);
1501    parser.addExclusiveArgumentSet(modifyEntryWithDN,
1502         suppressReferentialIntegrityUpdates);
1503    parser.addExclusiveArgumentSet(modifyEntryWithDN, addControl);
1504    parser.addExclusiveArgumentSet(modifyEntryWithDN, deleteControl);
1505    parser.addExclusiveArgumentSet(modifyEntryWithDN, modifyDNControl);
1506
1507    // The modifyEntriesWithDNsFromFile argument is incompatible with a lot of
1508    // settings, since it can only be used for modify operations.
1509    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, allowUndelete);
1510    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, defaultAdd);
1511    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, dryRun);
1512    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, hardDelete);
1513    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile,
1514         ignoreNoUserModification);
1515    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile,
1516         nameWithEntryUUID);
1517    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, softDelete);
1518    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile,
1519         clientSideSubtreeDelete);
1520    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile,
1521         serverSideSubtreeDelete);
1522    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile,
1523         suppressReferentialIntegrityUpdates);
1524    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, addControl);
1525    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile, deleteControl);
1526    parser.addExclusiveArgumentSet(modifyEntriesWithDNsFromFile,
1527         modifyDNControl);
1528  }
1529
1530
1531
1532  /**
1533   * {@inheritDoc}
1534   */
1535  @Override()
1536  public void doExtendedNonLDAPArgumentValidation()
1537         throws ArgumentException
1538  {
1539    // If we should use the route to backend set request control, then validate
1540    // and pre-create those controls.
1541    if (routeToBackendSet.isPresent())
1542    {
1543      final List<String> values = routeToBackendSet.getValues();
1544      final Map<String,List<String>> idsByRP = new LinkedHashMap<>(
1545           StaticUtils.computeMapCapacity(values.size()));
1546      for (final String value : values)
1547      {
1548        final int colonPos = value.indexOf(':');
1549        if (colonPos <= 0)
1550        {
1551          throw new ArgumentException(
1552               ERR_LDAPMODIFY_ROUTE_TO_BACKEND_SET_INVALID_FORMAT.get(value,
1553                    routeToBackendSet.getIdentifierString()));
1554        }
1555
1556        final String rpID = value.substring(0, colonPos);
1557        final String bsID = value.substring(colonPos+1);
1558
1559        List<String> idsForRP = idsByRP.get(rpID);
1560        if (idsForRP == null)
1561        {
1562          idsForRP = new ArrayList<>(values.size());
1563          idsByRP.put(rpID, idsForRP);
1564        }
1565        idsForRP.add(bsID);
1566      }
1567
1568      for (final Map.Entry<String,List<String>> e : idsByRP.entrySet())
1569      {
1570        final String rpID = e.getKey();
1571        final List<String> bsIDs = e.getValue();
1572        routeToBackendSetRequestControls.add(
1573             RouteToBackendSetRequestControl.createAbsoluteRoutingRequest(true,
1574                  rpID, bsIDs));
1575      }
1576    }
1577
1578
1579    // If the --proxyAs argument was provided, then make sure its value is
1580    // properly formatted.
1581    if (proxyAs.isPresent())
1582    {
1583      final String proxyAsValue = proxyAs.getValue();
1584      final String lowerProxyAsValue = StaticUtils.toLowerCase(proxyAsValue);
1585      if (lowerProxyAsValue.startsWith("dn:"))
1586      {
1587        final String dnString = proxyAsValue.substring(3);
1588        if (! DN.isValidDN(dnString))
1589        {
1590          throw new ArgumentException(ERR_LDAPMODIFY_PROXY_AS_DN_NOT_DN.get(
1591               proxyAs.getIdentifierString(), dnString));
1592        }
1593      }
1594      else if (! lowerProxyAsValue.startsWith("u:"))
1595      {
1596        throw new ArgumentException(
1597             ERR_LDAPMODIFY_PROXY_AS_VALUE_MISSING_PREFIX.get(
1598                  proxyAs.getIdentifierString()));
1599      }
1600    }
1601  }
1602
1603
1604
1605  /**
1606   * {@inheritDoc}
1607   */
1608  @Override()
1609  @NotNull()
1610  protected List<Control> getBindControls()
1611  {
1612    final ArrayList<Control> bindControls = new ArrayList<>(10);
1613
1614    if (bindControl.isPresent())
1615    {
1616      bindControls.addAll(bindControl.getValues());
1617    }
1618
1619    if (authorizationIdentity.isPresent())
1620    {
1621      bindControls.add(new AuthorizationIdentityRequestControl(false));
1622    }
1623
1624    if (generateAccessToken.isPresent())
1625    {
1626      bindControls.add(new GenerateAccessTokenRequestControl());
1627    }
1628
1629    if (getAuthorizationEntryAttribute.isPresent())
1630    {
1631      bindControls.add(new GetAuthorizationEntryRequestControl(true, true,
1632           getAuthorizationEntryAttribute.getValues()));
1633    }
1634
1635    if (getRecentLoginHistory.isPresent())
1636    {
1637      bindControls.add(new GetRecentLoginHistoryRequestControl());
1638    }
1639
1640    if (getUserResourceLimits.isPresent())
1641    {
1642      bindControls.add(new GetUserResourceLimitsRequestControl());
1643    }
1644
1645    if (usePasswordPolicyControl.isPresent())
1646    {
1647      bindControls.add(new PasswordPolicyRequestControl());
1648    }
1649
1650    if (suppressOperationalAttributeUpdates.isPresent())
1651    {
1652      final EnumSet<SuppressType> suppressTypes =
1653           EnumSet.noneOf(SuppressType.class);
1654      for (final String s : suppressOperationalAttributeUpdates.getValues())
1655      {
1656        if (s.equalsIgnoreCase("last-access-time"))
1657        {
1658          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
1659        }
1660        else if (s.equalsIgnoreCase("last-login-time"))
1661        {
1662          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
1663        }
1664        else if (s.equalsIgnoreCase("last-login-ip"))
1665        {
1666          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
1667        }
1668      }
1669
1670      bindControls.add(new SuppressOperationalAttributeUpdateRequestControl(
1671           suppressTypes));
1672    }
1673
1674    if (useJSONFormattedRequestControls.isPresent())
1675    {
1676      final JSONFormattedRequestControl jsonFormattedRequestControl =
1677           JSONFormattedRequestControl.createWithControls(true, bindControls);
1678      bindControls.clear();
1679      bindControls.add(jsonFormattedRequestControl);
1680    }
1681
1682    return bindControls;
1683  }
1684
1685
1686
1687  /**
1688   * {@inheritDoc}
1689   */
1690  @Override()
1691  protected boolean supportsMultipleServers()
1692  {
1693    // We will support providing information about multiple servers.  This tool
1694    // will not communicate with multiple servers concurrently, but it can
1695    // accept information about multiple servers in the event that a large set
1696    // of changes is to be processed and a server goes down in the middle of
1697    // those changes.  In this case, we can resume processing on a newly-created
1698    // connection, possibly to a different server.
1699    return true;
1700  }
1701
1702
1703
1704  /**
1705   * {@inheritDoc}
1706   */
1707  @Override()
1708  @NotNull()
1709  public LDAPConnectionOptions getConnectionOptions()
1710  {
1711    final LDAPConnectionOptions options = new LDAPConnectionOptions();
1712
1713    options.setUseSynchronousMode(true);
1714    options.setFollowReferrals(followReferrals.isPresent());
1715    options.setUnsolicitedNotificationHandler(this);
1716    options.setResponseTimeoutMillis(0L);
1717
1718    return options;
1719  }
1720
1721
1722
1723  /**
1724   * {@inheritDoc}
1725   */
1726  @Override()
1727  @NotNull()
1728  public ResultCode doToolProcessing()
1729  {
1730    // Examine the arguments to determine the sets of controls to use for each
1731    // type of request.
1732    final ArrayList<Control> addControls = new ArrayList<>(10);
1733    final ArrayList<Control> deleteControls = new ArrayList<>(10);
1734    final ArrayList<Control> modifyControls = new ArrayList<>(10);
1735    final ArrayList<Control> modifyDNControls = new ArrayList<>(10);
1736    final ArrayList<Control> searchControls = new ArrayList<>(10);
1737    try
1738    {
1739      createRequestControls(addControls, deleteControls, modifyControls,
1740           modifyDNControls, searchControls);
1741    }
1742    catch (final LDAPException le)
1743    {
1744      Debug.debugException(le);
1745      for (final String line :
1746           ResultUtils.formatResult(le, true, 0, WRAP_COLUMN))
1747      {
1748        err(line);
1749      }
1750      return le.getResultCode();
1751    }
1752
1753
1754    // If an encryption passphrase file was specified, then read its value.
1755    String encryptionPassphrase = null;
1756    if (encryptionPassphraseFile.isPresent())
1757    {
1758      try
1759      {
1760        encryptionPassphrase = ToolUtils.readEncryptionPassphraseFromFile(
1761             encryptionPassphraseFile.getValue());
1762      }
1763      catch (final LDAPException e)
1764      {
1765        Debug.debugException(e);
1766        wrapErr(0, WRAP_COLUMN, e.getMessage());
1767        return e.getResultCode();
1768      }
1769    }
1770
1771
1772    LDAPConnectionPool connectionPool = null;
1773    LDIFReader         ldifReader     = null;
1774    LDIFWriter         rejectWriter   = null;
1775    try
1776    {
1777      // Create a connection pool that will be used to communicate with the
1778      // directory server.  If we should use an administrative session, then
1779      // create a connect processor that will be used to start the session
1780      // before performing the bind.
1781      try
1782      {
1783        final StartAdministrativeSessionPostConnectProcessor p;
1784        if (useAdministrativeSession.isPresent())
1785        {
1786          p = new StartAdministrativeSessionPostConnectProcessor(
1787               new StartAdministrativeSessionExtendedRequest(getToolName(),
1788                    true));
1789        }
1790        else
1791        {
1792          p = null;
1793        }
1794
1795        if (! dryRun.isPresent())
1796        {
1797          connectionPool = getConnectionPool(1, 2, 0, p, null, true,
1798               new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
1799                    verbose.isPresent()));
1800        }
1801      }
1802      catch (final LDAPException le)
1803      {
1804        Debug.debugException(le);
1805
1806        // Unable to create the connection pool, which means that either the
1807        // connection could not be established or the attempt to authenticate
1808        // the connection failed.  If the bind failed, then the report bind
1809        // result health check should have already reported the bind failure.
1810        // If the failure was something else, then display that failure result.
1811        if (le.getResultCode() != ResultCode.INVALID_CREDENTIALS)
1812        {
1813          for (final String line :
1814               ResultUtils.formatResult(le, true, 0, WRAP_COLUMN))
1815          {
1816            err(line);
1817          }
1818        }
1819        return le.getResultCode();
1820      }
1821
1822      if (connectionPool != null)
1823      {
1824        connectionPool.setRetryFailedOperationsDueToInvalidConnections(
1825             (! neverRetry.isPresent()));
1826      }
1827
1828
1829      // Report that the connection was successfully established.
1830      if (connectionPool != null)
1831      {
1832        try
1833        {
1834          final LDAPConnection connection = connectionPool.getConnection();
1835          final String hostPort = connection.getHostPort();
1836          connectionPool.releaseConnection(connection);
1837          commentToOut(INFO_LDAPMODIFY_CONNECTION_ESTABLISHED.get(hostPort));
1838          out();
1839        }
1840        catch (final LDAPException le)
1841        {
1842          Debug.debugException(le);
1843          // This should never happen.
1844        }
1845      }
1846
1847
1848      // If we should process the operations in a transaction, then start that
1849      // now.
1850      final ASN1OctetString txnID;
1851      if (useTransaction.isPresent())
1852      {
1853        final Control[] startTxnControls;
1854        if (proxyAs.isPresent())
1855        {
1856          // In a transaction, the proxied authorization control must only be
1857          // used in the start transaction request and not in any of the
1858          // subsequent operation requests.
1859          startTxnControls = new Control[]
1860          {
1861            new ProxiedAuthorizationV2RequestControl(proxyAs.getValue())
1862          };
1863        }
1864        else if (proxyV1As.isPresent())
1865        {
1866          // In a transaction, the proxied authorization control must only be
1867          // used in the start transaction request and not in any of the
1868          // subsequent operation requests.
1869          startTxnControls = new Control[]
1870          {
1871            new ProxiedAuthorizationV1RequestControl(proxyV1As.getValue())
1872          };
1873        }
1874        else
1875        {
1876          startTxnControls = StaticUtils.NO_CONTROLS;
1877        }
1878
1879        try
1880        {
1881          final StartTransactionExtendedResult startTxnResult =
1882               (StartTransactionExtendedResult)
1883               connectionPool.processExtendedOperation(
1884                    new StartTransactionExtendedRequest(startTxnControls));
1885          if (startTxnResult.getResultCode() == ResultCode.SUCCESS)
1886          {
1887            txnID = startTxnResult.getTransactionID();
1888
1889            final TransactionSpecificationRequestControl c =
1890                 new TransactionSpecificationRequestControl(txnID);
1891            addControls.add(c);
1892            deleteControls.add(c);
1893            modifyControls.add(c);
1894            modifyDNControls.add(c);
1895
1896            final String txnIDString;
1897            if (StaticUtils.isPrintableString(txnID.getValue()))
1898            {
1899              txnIDString = txnID.stringValue();
1900            }
1901            else
1902            {
1903              final StringBuilder hexBuffer = new StringBuilder();
1904              StaticUtils.toHex(txnID.getValue(), ":", hexBuffer);
1905              txnIDString = hexBuffer.toString();
1906            }
1907
1908            commentToOut(INFO_LDAPMODIFY_STARTED_TXN.get(txnIDString));
1909          }
1910          else
1911          {
1912            commentToErr(ERR_LDAPMODIFY_CANNOT_START_TXN.get(
1913                 startTxnResult.getResultString()));
1914            return startTxnResult.getResultCode();
1915          }
1916        }
1917        catch (final LDAPException le)
1918        {
1919          Debug.debugException(le);
1920          commentToErr(ERR_LDAPMODIFY_CANNOT_START_TXN.get(
1921               StaticUtils.getExceptionMessage(le)));
1922          return le.getResultCode();
1923        }
1924      }
1925      else
1926      {
1927        txnID = null;
1928      }
1929
1930
1931      // Create an LDIF reader that will be used to read the changes to process.
1932      try
1933      {
1934        final InputStream ldifInputStream;
1935        if (ldifFile.isPresent())
1936        {
1937          ldifInputStream = ToolUtils.getInputStreamForLDIFFiles(
1938               ldifFile.getValues(), encryptionPassphrase, getOut(),
1939               getErr()).getFirst();
1940        }
1941        else
1942        {
1943          ldifInputStream = in;
1944        }
1945
1946        ldifReader = new LDIFReader(ldifInputStream, 0, null, null,
1947             characterSet.getValue());
1948      }
1949      catch (final Exception e)
1950      {
1951        commentToErr(ERR_LDAPMODIFY_CANNOT_CREATE_LDIF_READER.get(
1952             StaticUtils.getExceptionMessage(e)));
1953        return ResultCode.LOCAL_ERROR;
1954      }
1955
1956      if (stripTrailingSpaces.isPresent())
1957      {
1958        ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP);
1959      }
1960
1961
1962      // If appropriate, create a reject writer.
1963      if (rejectFile.isPresent())
1964      {
1965        try
1966        {
1967          rejectWriter = new LDIFWriter(rejectFile.getValue());
1968
1969          // Set the maximum allowed wrap column.  This is better than setting a
1970          // wrap column of zero because it will ensure that comments don't get
1971          // wrapped either.
1972          rejectWriter.setWrapColumn(Integer.MAX_VALUE);
1973        }
1974        catch (final Exception e)
1975        {
1976          Debug.debugException(e);
1977          commentToErr(ERR_LDAPMODIFY_CANNOT_CREATE_REJECT_WRITER.get(
1978               rejectFile.getValue().getAbsolutePath(),
1979               StaticUtils.getExceptionMessage(e)));
1980          return ResultCode.LOCAL_ERROR;
1981        }
1982      }
1983
1984
1985      // If appropriate, create a rate limiter.
1986      final FixedRateBarrier rateLimiter;
1987      if (ratePerSecond.isPresent())
1988      {
1989        rateLimiter = new FixedRateBarrier(1000L, ratePerSecond.getValue());
1990      }
1991      else
1992      {
1993        rateLimiter = null;
1994      }
1995
1996
1997      // Iterate through the set of changes to process.
1998      boolean commitTransaction = true;
1999      ResultCode resultCode = null;
2000      final ArrayList<LDAPRequest> multiUpdateRequests =
2001           new ArrayList<>(10);
2002      final boolean isBulkModify = modifyEntriesMatchingFilter.isPresent() ||
2003           modifyEntriesMatchingFiltersFromFile.isPresent() ||
2004           modifyEntryWithDN.isPresent() ||
2005           modifyEntriesWithDNsFromFile.isPresent();
2006readChangeRecordLoop:
2007      while (true)
2008      {
2009        // If there is a rate limiter, then use it to sleep if necessary.
2010        if ((rateLimiter != null) && (! isBulkModify))
2011        {
2012          rateLimiter.await();
2013        }
2014
2015
2016        // Read the next LDIF change record.  If we get an error then handle it
2017        // and abort if appropriate.
2018        final LDIFChangeRecord changeRecord;
2019        try
2020        {
2021          changeRecord = ldifReader.readChangeRecord(defaultAdd.isPresent());
2022        }
2023        catch (final IOException ioe)
2024        {
2025          Debug.debugException(ioe);
2026
2027          final String message = ERR_LDAPMODIFY_IO_ERROR_READING_CHANGE.get(
2028               StaticUtils.getExceptionMessage(ioe));
2029          commentToErr(message);
2030          writeRejectedChange(rejectWriter, message, null);
2031          commitTransaction = false;
2032          resultCode = ResultCode.LOCAL_ERROR;
2033          break;
2034        }
2035        catch (final LDIFException le)
2036        {
2037          Debug.debugException(le);
2038
2039          final StringBuilder buffer = new StringBuilder();
2040          if (le.mayContinueReading() && (! useTransaction.isPresent()))
2041          {
2042            buffer.append(
2043                 ERR_LDAPMODIFY_RECOVERABLE_LDIF_ERROR_READING_CHANGE.get(
2044                      le.getLineNumber(), StaticUtils.getExceptionMessage(le)));
2045          }
2046          else
2047          {
2048            buffer.append(
2049                 ERR_LDAPMODIFY_UNRECOVERABLE_LDIF_ERROR_READING_CHANGE.get(
2050                      le.getLineNumber(), StaticUtils.getExceptionMessage(le)));
2051          }
2052
2053          if ((resultCode == null) || (resultCode == ResultCode.SUCCESS))
2054          {
2055            resultCode = ResultCode.LOCAL_ERROR;
2056          }
2057
2058          if ((le.getDataLines() != null) && (! le.getDataLines().isEmpty()))
2059          {
2060            buffer.append(StaticUtils.EOL);
2061            buffer.append(StaticUtils.EOL);
2062            buffer.append(ERR_LDAPMODIFY_INVALID_LINES.get());
2063            buffer.append(StaticUtils.EOL);
2064            for (final String s : le.getDataLines())
2065            {
2066              buffer.append(s);
2067              buffer.append(StaticUtils.EOL);
2068            }
2069          }
2070
2071          final String message = buffer.toString();
2072          commentToErr(message);
2073          writeRejectedChange(rejectWriter, message, null);
2074
2075          if (le.mayContinueReading() && (! useTransaction.isPresent()))
2076          {
2077            continue;
2078          }
2079          else
2080          {
2081            commitTransaction = false;
2082            resultCode = ResultCode.LOCAL_ERROR;
2083            break;
2084          }
2085        }
2086
2087
2088        // If we read a null change record, then there are no more changes to
2089        // process.  Otherwise, treat it appropriately based on the operation
2090        // type.
2091        if (changeRecord == null)
2092        {
2093          break;
2094        }
2095
2096
2097        // If we should modify entries matching a specified filter, then convert
2098        // the change record into a set of modifications.
2099        if (modifyEntriesMatchingFilter.isPresent())
2100        {
2101          for (final Filter filter : modifyEntriesMatchingFilter.getValues())
2102          {
2103            final ResultCode rc = handleModifyMatchingFilter(connectionPool,
2104                 changeRecord,
2105                 modifyEntriesMatchingFilter.getIdentifierString(),
2106                 filter, searchControls, modifyControls, rateLimiter,
2107                 rejectWriter);
2108            if (rc != ResultCode.SUCCESS)
2109            {
2110              if ((resultCode == null) || (resultCode == ResultCode.SUCCESS) ||
2111                   (resultCode == ResultCode.NO_OPERATION))
2112              {
2113                resultCode = rc;
2114              }
2115            }
2116          }
2117        }
2118
2119        if (modifyEntriesMatchingFiltersFromFile.isPresent())
2120        {
2121          for (final File f : modifyEntriesMatchingFiltersFromFile.getValues())
2122          {
2123            final FilterFileReader filterReader;
2124            try
2125            {
2126              filterReader = new FilterFileReader(f);
2127            }
2128            catch (final Exception e)
2129            {
2130              Debug.debugException(e);
2131              commentToErr(ERR_LDAPMODIFY_ERROR_OPENING_FILTER_FILE.get(
2132                   f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
2133              return ResultCode.LOCAL_ERROR;
2134            }
2135
2136            try
2137            {
2138              while (true)
2139              {
2140                final Filter filter;
2141                try
2142                {
2143                  filter = filterReader.readFilter();
2144                }
2145                catch (final IOException ioe)
2146                {
2147                  Debug.debugException(ioe);
2148                  commentToErr(ERR_LDAPMODIFY_IO_ERROR_READING_FILTER_FILE.get(
2149                       f.getAbsolutePath(),
2150                       StaticUtils.getExceptionMessage(ioe)));
2151                  return ResultCode.LOCAL_ERROR;
2152                }
2153                catch (final LDAPException le)
2154                {
2155                  Debug.debugException(le);
2156                  commentToErr(le.getMessage());
2157                  if (continueOnError.isPresent())
2158                  {
2159                    if ((resultCode == null) ||
2160                        (resultCode == ResultCode.SUCCESS) ||
2161                        (resultCode == ResultCode.NO_OPERATION))
2162                    {
2163                      resultCode = le.getResultCode();
2164                    }
2165                    continue;
2166                  }
2167                  else
2168                  {
2169                    return le.getResultCode();
2170                  }
2171                }
2172
2173                if (filter == null)
2174                {
2175                  break;
2176                }
2177
2178                final ResultCode rc = handleModifyMatchingFilter(connectionPool,
2179                     changeRecord,
2180                     modifyEntriesMatchingFiltersFromFile.getIdentifierString(),
2181                     filter, searchControls, modifyControls, rateLimiter,
2182                     rejectWriter);
2183                if (rc != ResultCode.SUCCESS)
2184                {
2185                  if ((resultCode == null) ||
2186                      (resultCode == ResultCode.SUCCESS) ||
2187                      (resultCode == ResultCode.NO_OPERATION))
2188                  {
2189                    resultCode = rc;
2190                  }
2191                }
2192              }
2193            }
2194            finally
2195            {
2196              try
2197              {
2198                filterReader.close();
2199              }
2200              catch (final Exception e)
2201              {
2202                Debug.debugException(e);
2203              }
2204            }
2205          }
2206        }
2207
2208        if (modifyEntryWithDN.isPresent())
2209        {
2210          for (final DN dn : modifyEntryWithDN.getValues())
2211          {
2212            final ResultCode rc = handleModifyWithDN(connectionPool,
2213                 changeRecord, modifyEntryWithDN.getIdentifierString(), dn,
2214                 modifyControls, rateLimiter, rejectWriter);
2215            if (rc != ResultCode.SUCCESS)
2216            {
2217              if ((resultCode == null) || (resultCode == ResultCode.SUCCESS) ||
2218                   (resultCode == ResultCode.NO_OPERATION))
2219              {
2220                resultCode = rc;
2221              }
2222            }
2223          }
2224        }
2225
2226        if (modifyEntriesWithDNsFromFile.isPresent())
2227        {
2228          for (final File f : modifyEntriesWithDNsFromFile.getValues())
2229          {
2230            final DNFileReader dnReader;
2231            try
2232            {
2233              dnReader = new DNFileReader(f);
2234            }
2235            catch (final Exception e)
2236            {
2237              Debug.debugException(e);
2238              commentToErr(ERR_LDAPMODIFY_ERROR_OPENING_DN_FILE.get(
2239                   f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
2240              return ResultCode.LOCAL_ERROR;
2241            }
2242
2243            try
2244            {
2245              while (true)
2246              {
2247                final DN dn;
2248                try
2249                {
2250                  dn = dnReader.readDN();
2251                }
2252                catch (final IOException ioe)
2253                {
2254                  Debug.debugException(ioe);
2255                  commentToErr(ERR_LDAPMODIFY_IO_ERROR_READING_DN_FILE.get(
2256                       f.getAbsolutePath(),
2257                       StaticUtils.getExceptionMessage(ioe)));
2258                  return ResultCode.LOCAL_ERROR;
2259                }
2260                catch (final LDAPException le)
2261                {
2262                  Debug.debugException(le);
2263                  commentToErr(le.getMessage());
2264                  if (continueOnError.isPresent())
2265                  {
2266                    if ((resultCode == null) ||
2267                        (resultCode == ResultCode.SUCCESS) ||
2268                        (resultCode == ResultCode.NO_OPERATION))
2269                    {
2270                      resultCode = le.getResultCode();
2271                    }
2272                    continue;
2273                  }
2274                  else
2275                  {
2276                    return le.getResultCode();
2277                  }
2278                }
2279
2280                if (dn == null)
2281                {
2282                  break;
2283                }
2284
2285                final ResultCode rc = handleModifyWithDN(connectionPool,
2286                     changeRecord,
2287                     modifyEntriesWithDNsFromFile.getIdentifierString(), dn,
2288                     modifyControls, rateLimiter, rejectWriter);
2289                if (rc != ResultCode.SUCCESS)
2290                {
2291                  if ((resultCode == null) ||
2292                      (resultCode == ResultCode.SUCCESS) ||
2293                      (resultCode == ResultCode.NO_OPERATION))
2294                  {
2295                    resultCode = rc;
2296                  }
2297                }
2298              }
2299            }
2300            finally
2301            {
2302              try
2303              {
2304                dnReader.close();
2305              }
2306              catch (final Exception e)
2307              {
2308                Debug.debugException(e);
2309              }
2310            }
2311          }
2312        }
2313
2314        if (isBulkModify)
2315        {
2316          continue;
2317        }
2318
2319        try
2320        {
2321          final ResultCode rc;
2322          if (changeRecord instanceof LDIFAddChangeRecord)
2323          {
2324            rc = doAdd((LDIFAddChangeRecord) changeRecord, addControls,
2325                 connectionPool, multiUpdateRequests, rejectWriter);
2326          }
2327          else if (changeRecord instanceof LDIFDeleteChangeRecord)
2328          {
2329            rc = doDelete((LDIFDeleteChangeRecord) changeRecord, deleteControls,
2330                 connectionPool, multiUpdateRequests, rejectWriter);
2331          }
2332          else if (changeRecord instanceof LDIFModifyChangeRecord)
2333          {
2334            rc = doModify((LDIFModifyChangeRecord) changeRecord, modifyControls,
2335                 connectionPool, multiUpdateRequests, rejectWriter);
2336          }
2337          else if (changeRecord instanceof LDIFModifyDNChangeRecord)
2338          {
2339            rc = doModifyDN((LDIFModifyDNChangeRecord) changeRecord,
2340                 modifyDNControls, connectionPool, multiUpdateRequests,
2341                 rejectWriter);
2342          }
2343          else
2344          {
2345            // This should never happen.
2346            commentToErr(ERR_LDAPMODIFY_UNSUPPORTED_CHANGE_RECORD_HEADER.get());
2347            for (final String line : changeRecord.toLDIF())
2348            {
2349              err("#      " + line);
2350            }
2351            throw new LDAPException(ResultCode.PARAM_ERROR,
2352                 ERR_LDAPMODIFY_UNSUPPORTED_CHANGE_RECORD_HEADER.get() +
2353                      changeRecord.toString());
2354          }
2355
2356          if ((resultCode == null) && (rc != ResultCode.SUCCESS))
2357          {
2358            resultCode = rc;
2359          }
2360        }
2361        catch (final LDAPException le)
2362        {
2363          Debug.debugException(le);
2364
2365          commitTransaction = false;
2366          if (continueOnError.isPresent())
2367          {
2368            if ((resultCode == null) || (resultCode == ResultCode.SUCCESS) ||
2369                 (resultCode == ResultCode.NO_OPERATION))
2370            {
2371              resultCode = le.getResultCode();
2372            }
2373          }
2374          else
2375          {
2376            resultCode = le.getResultCode();
2377            break;
2378          }
2379        }
2380      }
2381
2382
2383      // If the operations are part of a transaction, then commit or abort that
2384      // transaction now.  Otherwise, if they should be part of a multi-update
2385      // operation, then process that now.
2386      if (useTransaction.isPresent())
2387      {
2388        LDAPResult endTxnResult;
2389        final EndTransactionExtendedRequest endTxnRequest =
2390             new EndTransactionExtendedRequest(txnID, commitTransaction);
2391        try
2392        {
2393          endTxnResult = connectionPool.processExtendedOperation(endTxnRequest);
2394        }
2395        catch (final LDAPException le)
2396        {
2397          endTxnResult = le.toLDAPResult();
2398        }
2399
2400        displayResult(endTxnResult, false);
2401        if (((resultCode == null) || (resultCode == ResultCode.SUCCESS)) &&
2402            (endTxnResult.getResultCode() != ResultCode.SUCCESS))
2403        {
2404          resultCode = endTxnResult.getResultCode();
2405        }
2406      }
2407      else if (multiUpdateErrorBehavior.isPresent())
2408      {
2409        final MultiUpdateErrorBehavior errorBehavior;
2410        if (multiUpdateErrorBehavior.getValue().equalsIgnoreCase("atomic"))
2411        {
2412          errorBehavior = MultiUpdateErrorBehavior.ATOMIC;
2413        }
2414        else if (multiUpdateErrorBehavior.getValue().equalsIgnoreCase(
2415                      "abort-on-error"))
2416        {
2417          errorBehavior = MultiUpdateErrorBehavior.ABORT_ON_ERROR;
2418        }
2419        else
2420        {
2421          errorBehavior = MultiUpdateErrorBehavior.CONTINUE_ON_ERROR;
2422        }
2423
2424        final Control[] multiUpdateControls;
2425        if (proxyAs.isPresent())
2426        {
2427          multiUpdateControls = new Control[]
2428          {
2429            new ProxiedAuthorizationV2RequestControl(proxyAs.getValue())
2430          };
2431        }
2432        else if (proxyV1As.isPresent())
2433        {
2434          multiUpdateControls = new Control[]
2435          {
2436            new ProxiedAuthorizationV1RequestControl(proxyV1As.getValue())
2437          };
2438        }
2439        else
2440        {
2441          multiUpdateControls = StaticUtils.NO_CONTROLS;
2442        }
2443
2444        ExtendedResult multiUpdateResult;
2445        try
2446        {
2447          commentToOut(INFO_LDAPMODIFY_SENDING_MULTI_UPDATE_REQUEST.get());
2448          final MultiUpdateExtendedRequest multiUpdateRequest =
2449               new MultiUpdateExtendedRequest(errorBehavior,
2450                    multiUpdateRequests, multiUpdateControls);
2451          multiUpdateResult =
2452               connectionPool.processExtendedOperation(multiUpdateRequest);
2453        }
2454        catch (final LDAPException le)
2455        {
2456          multiUpdateResult = new ExtendedResult(le);
2457        }
2458
2459        displayResult(multiUpdateResult, false);
2460        resultCode = multiUpdateResult.getResultCode();
2461      }
2462
2463
2464      if (resultCode == null)
2465      {
2466        return ResultCode.SUCCESS;
2467      }
2468      else
2469      {
2470        return resultCode;
2471      }
2472    }
2473    finally
2474    {
2475      if (rejectWriter != null)
2476      {
2477        try
2478        {
2479          rejectWriter.close();
2480        }
2481        catch (final Exception e)
2482        {
2483          Debug.debugException(e);
2484        }
2485      }
2486
2487      if (ldifReader != null)
2488      {
2489        try
2490        {
2491          ldifReader.close();
2492        }
2493        catch (final Exception e)
2494        {
2495          Debug.debugException(e);
2496        }
2497      }
2498
2499      if (connectionPool != null)
2500      {
2501        try
2502        {
2503          connectionPool.close();
2504        }
2505        catch (final Exception e)
2506        {
2507          Debug.debugException(e);
2508        }
2509      }
2510    }
2511  }
2512
2513
2514
2515  /**
2516   * Handles the processing for a change record when the tool should modify
2517   * entries matching a given filter.
2518   *
2519   * @param  connectionPool       The connection pool to use to communicate with
2520   *                              the directory server.
2521   * @param  changeRecord         The LDIF change record to be processed.
2522   * @param  argIdentifierString  The identifier string for the argument used to
2523   *                              specify the filter to use to identify the
2524   *                              entries to modify.
2525   * @param  filter               The filter to use to identify the entries to
2526   *                              modify.
2527   * @param  searchControls       The set of controls to include in the search
2528   *                              request.
2529   * @param  modifyControls       The set of controls to include in the modify
2530   *                              requests.
2531   * @param  rateLimiter          The fixed-rate barrier to use for rate
2532   *                              limiting.  It may be {@code null} if no rate
2533   *                              limiting is required.
2534   * @param  rejectWriter         The reject writer to use to record information
2535   *                              about any failed operations.
2536   *
2537   * @return  A result code obtained from processing.
2538   */
2539  @NotNull()
2540  private ResultCode handleModifyMatchingFilter(
2541               @NotNull final LDAPConnectionPool connectionPool,
2542               @NotNull final LDIFChangeRecord changeRecord,
2543               @NotNull final String argIdentifierString,
2544               @NotNull final Filter filter,
2545               @NotNull final List<Control> searchControls,
2546               @NotNull final List<Control> modifyControls,
2547               @Nullable final FixedRateBarrier rateLimiter,
2548               @Nullable final LDIFWriter rejectWriter)
2549  {
2550    // If the provided change record isn't a modify change record, then that's
2551    // an error.  Reject it.
2552    if (! (changeRecord instanceof LDIFModifyChangeRecord))
2553    {
2554      writeRejectedChange(rejectWriter,
2555           ERR_LDAPMODIFY_NON_MODIFY_WITH_BULK.get(argIdentifierString),
2556           changeRecord);
2557      return ResultCode.PARAM_ERROR;
2558    }
2559
2560    final LDIFModifyChangeRecord modifyChangeRecord =
2561         (LDIFModifyChangeRecord) changeRecord;
2562    final HashSet<DN> processedDNs =
2563         new HashSet<>(StaticUtils.computeMapCapacity(100));
2564
2565
2566    // If we need to use the simple paged results control, then we may have to
2567    // issue multiple searches.
2568    ASN1OctetString pagedResultsCookie = null;
2569    long entriesProcessed = 0L;
2570    ResultCode resultCode = ResultCode.SUCCESS;
2571    while (true)
2572    {
2573      // Construct the search request to send.
2574      final LDAPModifySearchListener listener =
2575           new LDAPModifySearchListener(this, modifyChangeRecord, filter,
2576                modifyControls, connectionPool, rateLimiter, rejectWriter,
2577                processedDNs);
2578
2579      final SearchRequest searchRequest =
2580           new SearchRequest(listener, modifyChangeRecord.getDN(),
2581                SearchScope.SUB, filter, SearchRequest.NO_ATTRIBUTES);
2582      searchRequest.setControls(searchControls);
2583      if (searchPageSize.isPresent())
2584      {
2585        searchRequest.addControl(new SimplePagedResultsControl(
2586             searchPageSize.getValue(), pagedResultsCookie));
2587      }
2588
2589
2590      // The connection pool's automatic retry feature can't work for searches
2591      // that return one or more entries before encountering a failure.  To get
2592      // around that, we'll check a connection out of the pool and use it to
2593      // process the search.  If an error occurs that indicates the connection
2594      // is no longer valid, we can replace it with a newly-established
2595      // connection and try again.  The search result listener will ensure that
2596      // no entry gets updated twice.
2597      LDAPConnection connection;
2598      try
2599      {
2600        connection = connectionPool.getConnection();
2601      }
2602      catch (final LDAPException le)
2603      {
2604        Debug.debugException(le);
2605
2606        writeRejectedChange(rejectWriter,
2607             ERR_LDAPMODIFY_CANNOT_GET_SEARCH_CONNECTION.get(
2608                  modifyChangeRecord.getDN(), String.valueOf(filter),
2609                  StaticUtils.getExceptionMessage(le)),
2610             modifyChangeRecord, le.toLDAPResult());
2611        return le.getResultCode();
2612      }
2613
2614      SearchResult searchResult;
2615      boolean connectionValid = false;
2616      try
2617      {
2618        try
2619        {
2620          searchResult = connection.search(searchRequest);
2621        }
2622        catch (final LDAPSearchException lse)
2623        {
2624          searchResult = lse.getSearchResult();
2625        }
2626
2627        if (searchResult.getResultCode() == ResultCode.SUCCESS)
2628        {
2629          connectionValid = true;
2630        }
2631        else if (searchResult.getResultCode().isConnectionUsable())
2632        {
2633          connectionValid = true;
2634          writeRejectedChange(rejectWriter,
2635               ERR_LDAPMODIFY_SEARCH_FAILED.get(modifyChangeRecord.getDN(),
2636                    String.valueOf(filter)),
2637               modifyChangeRecord, searchResult);
2638          return searchResult.getResultCode();
2639        }
2640        else if (! neverRetry.isPresent())
2641        {
2642          try
2643          {
2644            connection = connectionPool.replaceDefunctConnection(connection);
2645          }
2646          catch (final LDAPException le)
2647          {
2648            Debug.debugException(le);
2649            writeRejectedChange(rejectWriter,
2650                 ERR_LDAPMODIFY_SEARCH_FAILED_CANNOT_RECONNECT.get(
2651                      modifyChangeRecord.getDN(), String.valueOf(filter)),
2652                 modifyChangeRecord, searchResult);
2653            return searchResult.getResultCode();
2654          }
2655
2656          try
2657          {
2658            searchResult = connection.search(searchRequest);
2659          }
2660          catch (final LDAPSearchException lse)
2661          {
2662            Debug.debugException(lse);
2663            searchResult = lse.getSearchResult();
2664          }
2665
2666          if (searchResult.getResultCode() == ResultCode.SUCCESS)
2667          {
2668            connectionValid = true;
2669          }
2670          else
2671          {
2672            connectionValid = searchResult.getResultCode().isConnectionUsable();
2673            writeRejectedChange(rejectWriter,
2674                 ERR_LDAPMODIFY_SEARCH_FAILED.get(modifyChangeRecord.getDN(),
2675                      String.valueOf(filter)),
2676                 modifyChangeRecord, searchResult);
2677            return searchResult.getResultCode();
2678          }
2679        }
2680        else
2681        {
2682          writeRejectedChange(rejectWriter,
2683               ERR_LDAPMODIFY_SEARCH_FAILED.get(modifyChangeRecord.getDN(),
2684                    String.valueOf(filter)),
2685               modifyChangeRecord, searchResult);
2686          return searchResult.getResultCode();
2687        }
2688      }
2689      finally
2690      {
2691        if (connectionValid)
2692        {
2693          connectionPool.releaseConnection(connection);
2694        }
2695        else
2696        {
2697          connectionPool.releaseDefunctConnection(connection);
2698        }
2699      }
2700
2701      searchResult = LDAPSearch.handleJSONEncodedResponseControls(searchResult);
2702
2703
2704      // If we've gotten here, then the search was successful.  Check to see if
2705      // any of the modifications failed, and if so then update the result code
2706      // accordingly.
2707      if ((resultCode == ResultCode.SUCCESS) &&
2708          (listener.getResultCode() != ResultCode.SUCCESS))
2709      {
2710        resultCode = listener.getResultCode();
2711      }
2712
2713
2714      // If the search used the simple paged results control then we may need to
2715      // repeat the search to get the next page.
2716      entriesProcessed += searchResult.getEntryCount();
2717      if (searchPageSize.isPresent())
2718      {
2719        final SimplePagedResultsControl responseControl;
2720        try
2721        {
2722          responseControl = SimplePagedResultsControl.get(searchResult);
2723        }
2724        catch (final LDAPException le)
2725        {
2726          Debug.debugException(le);
2727          writeRejectedChange(rejectWriter,
2728               ERR_LDAPMODIFY_CANNOT_DECODE_PAGED_RESULTS_CONTROL.get(
2729                    modifyChangeRecord.getDN(), String.valueOf(filter)),
2730               modifyChangeRecord, le.toLDAPResult());
2731          return le.getResultCode();
2732        }
2733
2734        if (responseControl == null)
2735        {
2736          writeRejectedChange(rejectWriter,
2737               ERR_LDAPMODIFY_MISSING_PAGED_RESULTS_RESPONSE.get(
2738                    modifyChangeRecord.getDN(), String.valueOf(filter)),
2739               modifyChangeRecord);
2740          return ResultCode.CONTROL_NOT_FOUND;
2741        }
2742        else
2743        {
2744          pagedResultsCookie = responseControl.getCookie();
2745          if (responseControl.moreResultsToReturn())
2746          {
2747            if (verbose.isPresent())
2748            {
2749              commentToOut(INFO_LDAPMODIFY_SEARCH_COMPLETED_MORE_PAGES.get(
2750                   modifyChangeRecord.getDN(), String.valueOf(filter),
2751                   entriesProcessed));
2752              for (final String resultLine :
2753                   ResultUtils.formatResult(searchResult, true, 0, WRAP_COLUMN))
2754              {
2755                out(resultLine);
2756              }
2757              out();
2758            }
2759          }
2760          else
2761          {
2762            commentToOut(INFO_LDAPMODIFY_SEARCH_COMPLETED.get(
2763                 entriesProcessed, modifyChangeRecord.getDN(),
2764                 String.valueOf(filter)));
2765            if (verbose.isPresent())
2766            {
2767              for (final String resultLine :
2768                   ResultUtils.formatResult(searchResult, true, 0, WRAP_COLUMN))
2769              {
2770                out(resultLine);
2771              }
2772            }
2773
2774            out();
2775            return resultCode;
2776          }
2777        }
2778      }
2779      else
2780      {
2781        commentToOut(INFO_LDAPMODIFY_SEARCH_COMPLETED.get(
2782             entriesProcessed, modifyChangeRecord.getDN(),
2783             String.valueOf(filter)));
2784        if (verbose.isPresent())
2785        {
2786          for (final String resultLine :
2787               ResultUtils.formatResult(searchResult, true, 0, WRAP_COLUMN))
2788          {
2789            out(resultLine);
2790          }
2791        }
2792
2793        out();
2794        return resultCode;
2795      }
2796    }
2797  }
2798
2799
2800
2801  /**
2802   * Handles the processing for a change record when the tool should modify an
2803   * entry with a given DN instead of the DN contained in the change record.
2804   *
2805   * @param  connectionPool       The connection pool to use to communicate with
2806   *                              the directory server.
2807   * @param  changeRecord         The LDIF change record to be processed.
2808   * @param  argIdentifierString  The identifier string for the argument used to
2809   *                              specify the DN of the entry to modify.
2810   * @param  dn                   The DN of the entry to modify.
2811   * @param  modifyControls       The set of controls to include in the modify
2812   *                              requests.
2813   * @param  rateLimiter          The fixed-rate barrier to use for rate
2814   *                              limiting.  It may be {@code null} if no rate
2815   *                              limiting is required.
2816   * @param  rejectWriter         The reject writer to use to record information
2817   *                              about any failed operations.
2818   *
2819   * @return  A result code obtained from processing.
2820   */
2821  @NotNull()
2822  private ResultCode handleModifyWithDN(
2823               @NotNull final LDAPConnectionPool connectionPool,
2824               @NotNull final LDIFChangeRecord changeRecord,
2825               @NotNull final String argIdentifierString,
2826               @NotNull final DN dn,
2827               @NotNull final List<Control> modifyControls,
2828               @Nullable final FixedRateBarrier rateLimiter,
2829               @Nullable final LDIFWriter rejectWriter)
2830  {
2831    // If the provided change record isn't a modify change record, then that's
2832    // an error.  Reject it.
2833    if (! (changeRecord instanceof LDIFModifyChangeRecord))
2834    {
2835      writeRejectedChange(rejectWriter,
2836           ERR_LDAPMODIFY_NON_MODIFY_WITH_BULK.get(argIdentifierString),
2837           changeRecord);
2838      return ResultCode.PARAM_ERROR;
2839    }
2840
2841
2842    // Create a new modify change record with the provided DN instead of the
2843    // original DN.
2844    final LDIFModifyChangeRecord originalChangeRecord =
2845         (LDIFModifyChangeRecord) changeRecord;
2846    final LDIFModifyChangeRecord updatedChangeRecord =
2847         new LDIFModifyChangeRecord(dn.toString(),
2848              originalChangeRecord.getModifications(),
2849              originalChangeRecord.getControls());
2850
2851    if (rateLimiter != null)
2852    {
2853      rateLimiter.await();
2854    }
2855
2856    try
2857    {
2858      return doModify(updatedChangeRecord, modifyControls, connectionPool, null,
2859           rejectWriter);
2860    }
2861    catch (final LDAPException le)
2862    {
2863      Debug.debugException(le);
2864      return le.getResultCode();
2865    }
2866  }
2867
2868
2869
2870  /**
2871   * Populates lists of request controls that should be included in requests
2872   * of various types.
2873   *
2874   * @param  addControls       The list of controls to include in add requests.
2875   * @param  deleteControls    The list of controls to include in delete
2876   *                           requests.
2877   * @param  modifyControls    The list of controls to include in modify
2878   *                           requests.
2879   * @param  modifyDNControls  The list of controls to include in modify DN
2880   *                           requests.
2881   * @param  searchControls    The list of controls to include in search
2882   *                           requests.
2883   *
2884   * @throws  LDAPException  If a problem is encountered while creating any of
2885   *                         the requested controls.
2886   */
2887  private void createRequestControls(
2888                    @NotNull final List<Control> addControls,
2889                    @NotNull final List<Control> deleteControls,
2890                    @NotNull final List<Control> modifyControls,
2891                    @NotNull final List<Control> modifyDNControls,
2892                    @NotNull final List<Control> searchControls)
2893          throws LDAPException
2894  {
2895    if (addControl.isPresent())
2896    {
2897      addControls.addAll(addControl.getValues());
2898    }
2899
2900    if (deleteControl.isPresent())
2901    {
2902      deleteControls.addAll(deleteControl.getValues());
2903    }
2904
2905    if (modifyControl.isPresent())
2906    {
2907      modifyControls.addAll(modifyControl.getValues());
2908    }
2909
2910    if (modifyDNControl.isPresent())
2911    {
2912      modifyDNControls.addAll(modifyDNControl.getValues());
2913    }
2914
2915    if (accessLogField.isPresent())
2916    {
2917      final Map<String,JSONValue> fields = new LinkedHashMap<>();
2918      for (final String nameValueStr : accessLogField.getValues())
2919      {
2920        final int colonPos = nameValueStr.indexOf(':');
2921        if (colonPos < 0)
2922        {
2923          throw new LDAPException(ResultCode.PARAM_ERROR,
2924               ERR_LDAPMODIFY_ACCESS_LOG_FIELD_NO_COLON.get(
2925                    accessLogField.getIdentifierString(), nameValueStr));
2926        }
2927
2928        final String fieldName = nameValueStr.substring(0, colonPos);
2929        if (fields.containsKey(fieldName))
2930        {
2931          throw new LDAPException(ResultCode.PARAM_ERROR,
2932               ERR_LDAPMODIFY_ACCESS_LOG_FIELD_DUPLICATE_FIELD.get(
2933                    accessLogField.getIdentifierString(), fieldName));
2934        }
2935
2936        final String valueStr = nameValueStr.substring(colonPos + 1);
2937        if (valueStr.equalsIgnoreCase("true"))
2938        {
2939          fields.put(fieldName, JSONBoolean.TRUE);
2940        }
2941        else if (valueStr.equalsIgnoreCase("false"))
2942        {
2943          fields.put(fieldName, JSONBoolean.FALSE);
2944        }
2945        else
2946        {
2947          try
2948          {
2949            final BigDecimal d = new BigDecimal(valueStr);
2950            fields.put(fieldName, new JSONNumber(d));
2951          }
2952          catch (final Exception e)
2953          {
2954            Debug.debugException(e);
2955            fields.put(fieldName, new JSONString(valueStr));
2956          }
2957        }
2958      }
2959
2960      final AccessLogFieldRequestControl c =
2961           new AccessLogFieldRequestControl(false, new JSONObject(fields));
2962      addControls.add(c);
2963      deleteControls.add(c);
2964      modifyControls.add(c);
2965      modifyDNControls.add(c);
2966    }
2967
2968    if (operationControl.isPresent())
2969    {
2970      addControls.addAll(operationControl.getValues());
2971      deleteControls.addAll(operationControl.getValues());
2972      modifyControls.addAll(operationControl.getValues());
2973      modifyDNControls.addAll(operationControl.getValues());
2974    }
2975
2976    addControls.addAll(routeToBackendSetRequestControls);
2977    deleteControls.addAll(routeToBackendSetRequestControls);
2978    modifyControls.addAll(routeToBackendSetRequestControls);
2979    modifyDNControls.addAll(routeToBackendSetRequestControls);
2980
2981    if (noOperation.isPresent())
2982    {
2983      final NoOpRequestControl c = new NoOpRequestControl();
2984      addControls.add(c);
2985      deleteControls.add(c);
2986      modifyControls.add(c);
2987      modifyDNControls.add(c);
2988    }
2989
2990    if (generatePassword.isPresent())
2991    {
2992      addControls.add(new GeneratePasswordRequestControl());
2993    }
2994
2995    if (getBackendSetID.isPresent())
2996    {
2997      final GetBackendSetIDRequestControl c =
2998           new GetBackendSetIDRequestControl(false);
2999      addControls.add(c);
3000      deleteControls.add(c);
3001      modifyControls.add(c);
3002      modifyDNControls.add(c);
3003    }
3004
3005    if (getServerID.isPresent())
3006    {
3007      final GetServerIDRequestControl c =
3008           new GetServerIDRequestControl(false);
3009      addControls.add(c);
3010      deleteControls.add(c);
3011      modifyControls.add(c);
3012      modifyDNControls.add(c);
3013    }
3014
3015    if (ignoreNoUserModification.isPresent())
3016    {
3017      addControls.add(new IgnoreNoUserModificationRequestControl(false));
3018      modifyControls.add(new IgnoreNoUserModificationRequestControl(false));
3019    }
3020
3021    if (nameWithEntryUUID.isPresent())
3022    {
3023      addControls.add(new NameWithEntryUUIDRequestControl(true));
3024    }
3025
3026    if (permissiveModify.isPresent())
3027    {
3028      modifyControls.add(new PermissiveModifyRequestControl(false));
3029    }
3030
3031    if (routeToServer.isPresent())
3032    {
3033      final RouteToServerRequestControl c =
3034           new RouteToServerRequestControl(false,
3035           routeToServer.getValue(), false, false, false);
3036      addControls.add(c);
3037      deleteControls.add(c);
3038      modifyControls.add(c);
3039      modifyDNControls.add(c);
3040    }
3041
3042    if (suppressReferentialIntegrityUpdates.isPresent())
3043    {
3044      final SuppressReferentialIntegrityUpdatesRequestControl c =
3045           new SuppressReferentialIntegrityUpdatesRequestControl(true);
3046      deleteControls.add(c);
3047      modifyDNControls.add(c);
3048    }
3049
3050    if (suppressOperationalAttributeUpdates.isPresent())
3051    {
3052      final EnumSet<SuppressType> suppressTypes =
3053           EnumSet.noneOf(SuppressType.class);
3054      for (final String s : suppressOperationalAttributeUpdates.getValues())
3055      {
3056        if (s.equalsIgnoreCase("last-access-time"))
3057        {
3058          suppressTypes.add(SuppressType.LAST_ACCESS_TIME);
3059        }
3060        else if (s.equalsIgnoreCase("last-login-time"))
3061        {
3062          suppressTypes.add(SuppressType.LAST_LOGIN_TIME);
3063        }
3064        else if (s.equalsIgnoreCase("last-login-ip"))
3065        {
3066          suppressTypes.add(SuppressType.LAST_LOGIN_IP);
3067        }
3068        else if (s.equalsIgnoreCase("lastmod"))
3069        {
3070          suppressTypes.add(SuppressType.LASTMOD);
3071        }
3072      }
3073
3074      final SuppressOperationalAttributeUpdateRequestControl c =
3075           new SuppressOperationalAttributeUpdateRequestControl(suppressTypes);
3076      addControls.add(c);
3077      deleteControls.add(c);
3078      modifyControls.add(c);
3079      modifyDNControls.add(c);
3080    }
3081
3082    if (usePasswordPolicyControl.isPresent())
3083    {
3084      final PasswordPolicyRequestControl c = new PasswordPolicyRequestControl();
3085      addControls.add(c);
3086      modifyControls.add(c);
3087    }
3088
3089    if (assuredReplication.isPresent())
3090    {
3091      AssuredReplicationLocalLevel localLevel = null;
3092      if (assuredReplicationLocalLevel.isPresent())
3093      {
3094        final String level = assuredReplicationLocalLevel.getValue();
3095        if (level.equalsIgnoreCase("none"))
3096        {
3097          localLevel = AssuredReplicationLocalLevel.NONE;
3098        }
3099        else if (level.equalsIgnoreCase("received-any-server"))
3100        {
3101          localLevel = AssuredReplicationLocalLevel.RECEIVED_ANY_SERVER;
3102        }
3103        else if (level.equalsIgnoreCase("processed-all-servers"))
3104        {
3105          localLevel = AssuredReplicationLocalLevel.PROCESSED_ALL_SERVERS;
3106        }
3107      }
3108
3109      AssuredReplicationRemoteLevel remoteLevel = null;
3110      if (assuredReplicationRemoteLevel.isPresent())
3111      {
3112        final String level = assuredReplicationRemoteLevel.getValue();
3113        if (level.equalsIgnoreCase("none"))
3114        {
3115          remoteLevel = AssuredReplicationRemoteLevel.NONE;
3116        }
3117        else if (level.equalsIgnoreCase("received-any-remote-location"))
3118        {
3119          remoteLevel =
3120               AssuredReplicationRemoteLevel.RECEIVED_ANY_REMOTE_LOCATION;
3121        }
3122        else if (level.equalsIgnoreCase("received-all-remote-locations"))
3123        {
3124          remoteLevel =
3125               AssuredReplicationRemoteLevel.RECEIVED_ALL_REMOTE_LOCATIONS;
3126        }
3127        else if (level.equalsIgnoreCase("processed-all-remote-servers"))
3128        {
3129          remoteLevel =
3130               AssuredReplicationRemoteLevel.PROCESSED_ALL_REMOTE_SERVERS;
3131        }
3132      }
3133
3134      Long timeoutMillis = null;
3135      if (assuredReplicationTimeout.isPresent())
3136      {
3137        timeoutMillis =
3138             assuredReplicationTimeout.getValue(TimeUnit.MILLISECONDS);
3139      }
3140
3141      final AssuredReplicationRequestControl c =
3142           new AssuredReplicationRequestControl(true, localLevel, localLevel,
3143                remoteLevel, remoteLevel, timeoutMillis, false);
3144      addControls.add(c);
3145      deleteControls.add(c);
3146      modifyControls.add(c);
3147      modifyDNControls.add(c);
3148    }
3149
3150    if (hardDelete.isPresent() && (! clientSideSubtreeDelete.isPresent()))
3151    {
3152      deleteControls.add(new HardDeleteRequestControl(true));
3153    }
3154
3155    if (replicationRepair.isPresent())
3156    {
3157      final ReplicationRepairRequestControl c =
3158           new ReplicationRepairRequestControl();
3159      addControls.add(c);
3160      deleteControls.add(c);
3161      modifyControls.add(c);
3162      modifyDNControls.add(c);
3163    }
3164
3165    if (softDelete.isPresent())
3166    {
3167      deleteControls.add(new SoftDeleteRequestControl(true, true));
3168    }
3169
3170    if (serverSideSubtreeDelete.isPresent())
3171    {
3172      deleteControls.add(new SubtreeDeleteRequestControl());
3173    }
3174
3175    if (assertionFilter.isPresent())
3176    {
3177      final AssertionRequestControl c = new AssertionRequestControl(
3178           assertionFilter.getValue(), true);
3179      addControls.add(c);
3180      deleteControls.add(c);
3181      modifyControls.add(c);
3182      modifyDNControls.add(c);
3183    }
3184
3185    if (operationPurpose.isPresent())
3186    {
3187      final OperationPurposeRequestControl c =
3188           new OperationPurposeRequestControl(false, "ldapmodify",
3189                Version.NUMERIC_VERSION_STRING,
3190                LDAPModify.class.getName() + ".createRequestControls",
3191                operationPurpose.getValue());
3192      addControls.add(c);
3193      deleteControls.add(c);
3194      modifyControls.add(c);
3195      modifyDNControls.add(c);
3196    }
3197
3198    if (manageDsaIT.isPresent())
3199    {
3200      final ManageDsaITRequestControl c = new ManageDsaITRequestControl(true);
3201      addControls.add(c);
3202      if (! clientSideSubtreeDelete.isPresent())
3203      {
3204        deleteControls.add(c);
3205      }
3206      modifyControls.add(c);
3207      modifyDNControls.add(c);
3208    }
3209
3210    if (passwordUpdateBehavior.isPresent())
3211    {
3212      final PasswordUpdateBehaviorRequestControl c =
3213           createPasswordUpdateBehaviorRequestControl(
3214                passwordUpdateBehavior.getIdentifierString(),
3215                passwordUpdateBehavior.getValues());
3216      addControls.add(c);
3217      modifyControls.add(c);
3218    }
3219
3220    if (preReadAttribute.isPresent())
3221    {
3222      final ArrayList<String> attrList = new ArrayList<>(10);
3223      for (final String value : preReadAttribute.getValues())
3224      {
3225        final StringTokenizer tokenizer = new StringTokenizer(value, ", ");
3226        while (tokenizer.hasMoreTokens())
3227        {
3228          attrList.add(tokenizer.nextToken());
3229        }
3230      }
3231
3232      final String[] attrArray = attrList.toArray(StaticUtils.NO_STRINGS);
3233      final PreReadRequestControl c = new PreReadRequestControl(attrArray);
3234      deleteControls.add(c);
3235      modifyControls.add(c);
3236      modifyDNControls.add(c);
3237    }
3238
3239    if (postReadAttribute.isPresent())
3240    {
3241      final ArrayList<String> attrList = new ArrayList<>(10);
3242      for (final String value : postReadAttribute.getValues())
3243      {
3244        final StringTokenizer tokenizer = new StringTokenizer(value, ", ");
3245        while (tokenizer.hasMoreTokens())
3246        {
3247          attrList.add(tokenizer.nextToken());
3248        }
3249      }
3250
3251      final String[] attrArray = attrList.toArray(StaticUtils.NO_STRINGS);
3252      final PostReadRequestControl c = new PostReadRequestControl(attrArray);
3253      addControls.add(c);
3254      modifyControls.add(c);
3255      modifyDNControls.add(c);
3256    }
3257
3258    if (proxyAs.isPresent() && (! useTransaction.isPresent()) &&
3259        (! multiUpdateErrorBehavior.isPresent()))
3260    {
3261      final ProxiedAuthorizationV2RequestControl c =
3262           new ProxiedAuthorizationV2RequestControl(proxyAs.getValue());
3263      addControls.add(c);
3264      deleteControls.add(c);
3265      modifyControls.add(c);
3266      modifyDNControls.add(c);
3267      searchControls.add(c);
3268    }
3269
3270    if (proxyV1As.isPresent() && (! useTransaction.isPresent()) &&
3271        (! multiUpdateErrorBehavior.isPresent()))
3272    {
3273      final ProxiedAuthorizationV1RequestControl c =
3274           new ProxiedAuthorizationV1RequestControl(proxyV1As.getValue());
3275      addControls.add(c);
3276      deleteControls.add(c);
3277      modifyControls.add(c);
3278      modifyDNControls.add(c);
3279      searchControls.add(c);
3280    }
3281
3282    if (uniquenessAttribute.isPresent() || uniquenessFilter.isPresent())
3283    {
3284      final UniquenessRequestControlProperties uniquenessProperties;
3285      if (uniquenessAttribute.isPresent())
3286      {
3287        uniquenessProperties = new UniquenessRequestControlProperties(
3288             uniquenessAttribute.getValues());
3289        if (uniquenessFilter.isPresent())
3290        {
3291          uniquenessProperties.setFilter(uniquenessFilter.getValue());
3292        }
3293      }
3294      else
3295      {
3296        uniquenessProperties = new UniquenessRequestControlProperties(
3297             uniquenessFilter.getValue());
3298      }
3299
3300      if (uniquenessBaseDN.isPresent())
3301      {
3302        uniquenessProperties.setBaseDN(uniquenessBaseDN.getStringValue());
3303      }
3304
3305      if (uniquenessMultipleAttributeBehavior.isPresent())
3306      {
3307        final String value =
3308             uniquenessMultipleAttributeBehavior.getValue().toLowerCase();
3309        switch (value)
3310        {
3311          case "unique-within-each-attribute":
3312            uniquenessProperties.setMultipleAttributeBehavior(
3313                 UniquenessMultipleAttributeBehavior.
3314                      UNIQUE_WITHIN_EACH_ATTRIBUTE);
3315            break;
3316          case "unique-across-all-attributes-including-in-same-entry":
3317            uniquenessProperties.setMultipleAttributeBehavior(
3318                 UniquenessMultipleAttributeBehavior.
3319                      UNIQUE_ACROSS_ALL_ATTRIBUTES_INCLUDING_IN_SAME_ENTRY);
3320            break;
3321          case "unique-across-all-attributes-except-in-same-entry":
3322            uniquenessProperties.setMultipleAttributeBehavior(
3323                 UniquenessMultipleAttributeBehavior.
3324                      UNIQUE_ACROSS_ALL_ATTRIBUTES_EXCEPT_IN_SAME_ENTRY);
3325            break;
3326          case "unique-in-combination":
3327            uniquenessProperties.setMultipleAttributeBehavior(
3328                 UniquenessMultipleAttributeBehavior.UNIQUE_IN_COMBINATION);
3329            break;
3330        }
3331      }
3332
3333      if (uniquenessPreCommitValidationLevel.isPresent())
3334      {
3335        final String value =
3336             uniquenessPreCommitValidationLevel.getValue().toLowerCase();
3337        switch (value)
3338        {
3339          case "none":
3340            uniquenessProperties.setPreCommitValidationLevel(
3341                 UniquenessValidationLevel.NONE);
3342            break;
3343          case "all-subtree-views":
3344            uniquenessProperties.setPreCommitValidationLevel(
3345                 UniquenessValidationLevel.ALL_SUBTREE_VIEWS);
3346            break;
3347          case "all-backend-sets":
3348            uniquenessProperties.setPreCommitValidationLevel(
3349                 UniquenessValidationLevel.ALL_BACKEND_SETS);
3350            break;
3351          case "all-available-backend-servers":
3352            uniquenessProperties.setPreCommitValidationLevel(
3353                 UniquenessValidationLevel.ALL_AVAILABLE_BACKEND_SERVERS);
3354            break;
3355        }
3356      }
3357
3358      if (uniquenessPostCommitValidationLevel.isPresent())
3359      {
3360        final String value =
3361             uniquenessPostCommitValidationLevel.getValue().toLowerCase();
3362        switch (value)
3363        {
3364          case "none":
3365            uniquenessProperties.setPostCommitValidationLevel(
3366                 UniquenessValidationLevel.NONE);
3367            break;
3368          case "all-subtree-views":
3369            uniquenessProperties.setPostCommitValidationLevel(
3370                 UniquenessValidationLevel.ALL_SUBTREE_VIEWS);
3371            break;
3372          case "all-backend-sets":
3373            uniquenessProperties.setPostCommitValidationLevel(
3374                 UniquenessValidationLevel.ALL_BACKEND_SETS);
3375            break;
3376          case "all-available-backend-servers":
3377            uniquenessProperties.setPostCommitValidationLevel(
3378                 UniquenessValidationLevel.ALL_AVAILABLE_BACKEND_SERVERS);
3379            break;
3380        }
3381      }
3382
3383      final UniquenessRequestControl c =
3384           new UniquenessRequestControl(true, null, uniquenessProperties);
3385      addControls.add(c);
3386      modifyControls.add(c);
3387      modifyDNControls.add(c);
3388    }
3389
3390
3391    if (useJSONFormattedRequestControls.isPresent())
3392    {
3393      final JSONFormattedRequestControl jsonFormattedAddRequestControl =
3394           JSONFormattedRequestControl.createWithControls(true, addControls);
3395      addControls.clear();
3396      addControls.add(jsonFormattedAddRequestControl);
3397
3398      final JSONFormattedRequestControl jsonFormattedDeleteRequestControl =
3399           JSONFormattedRequestControl.createWithControls(true, deleteControls);
3400      deleteControls.clear();
3401      deleteControls.add(jsonFormattedDeleteRequestControl);
3402
3403      final JSONFormattedRequestControl jsonFormattedModifyRequestControl =
3404           JSONFormattedRequestControl.createWithControls(true, modifyControls);
3405      modifyControls.clear();
3406      modifyControls.add(jsonFormattedModifyRequestControl);
3407
3408      final JSONFormattedRequestControl jsonFormattedModifyDNRequestControl =
3409           JSONFormattedRequestControl.createWithControls(true,
3410                modifyDNControls);
3411      modifyDNControls.clear();
3412      modifyDNControls.add(jsonFormattedModifyDNRequestControl);
3413
3414      final JSONFormattedRequestControl jsonFormattedSearchRequestControl =
3415           JSONFormattedRequestControl.createWithControls(true, searchControls);
3416      searchControls.clear();
3417      searchControls.add(jsonFormattedSearchRequestControl);
3418    }
3419  }
3420
3421
3422
3423  /**
3424   * Creates the password update behavior request control that should be
3425   * included in add and modify requests.
3426   *
3427   * @param  argIdentifier  The identifier string for the argument used to
3428   *                        configure the password update behavior request
3429   *                        control.
3430   * @param  argValues      The set of values for the password update behavior
3431   *                        request control.
3432   *
3433   * @return  The password update behavior request control that was created.
3434   *
3435   * @throws  LDAPException  If a problem is encountered while creating the
3436   *                         control.
3437   */
3438  @NotNull()
3439  static PasswordUpdateBehaviorRequestControl
3440              createPasswordUpdateBehaviorRequestControl(
3441                   @NotNull final String argIdentifier,
3442                   @NotNull final List<String> argValues)
3443       throws LDAPException
3444  {
3445    final PasswordUpdateBehaviorRequestControlProperties properties =
3446         new PasswordUpdateBehaviorRequestControlProperties();
3447
3448    for (final String argValue : argValues)
3449    {
3450      int delimiterPos = argValue.indexOf('=');
3451      if (delimiterPos < 0)
3452      {
3453        delimiterPos = argValue.indexOf(':');
3454      }
3455
3456      if ((delimiterPos <= 0) || (delimiterPos >= (argValue.length() - 1)))
3457      {
3458        throw new LDAPException(ResultCode.PARAM_ERROR,
3459             ERR_LDAPMODIFY_MALFORMED_PW_UPDATE_BEHAVIOR.get(argValue,
3460                  argIdentifier));
3461      }
3462
3463      final String name = argValue.substring(0, delimiterPos).trim();
3464      final String value = argValue.substring(delimiterPos+1).trim();
3465      if (name.equalsIgnoreCase("is-self-change") ||
3466           name.equalsIgnoreCase("self-change") ||
3467           name.equalsIgnoreCase("isSelfChange") ||
3468           name.equalsIgnoreCase("selfChange"))
3469      {
3470        properties.setIsSelfChange(parseBooleanValue(name, value));
3471      }
3472      else if (name.equalsIgnoreCase("allow-pre-encoded-password") ||
3473           name.equalsIgnoreCase("allow-pre-encoded-passwords") ||
3474           name.equalsIgnoreCase("allow-pre-encoded") ||
3475           name.equalsIgnoreCase("allowPreEncodedPassword") ||
3476           name.equalsIgnoreCase("allowPreEncodedPasswords") ||
3477           name.equalsIgnoreCase("allowPreEncoded"))
3478      {
3479        properties.setAllowPreEncodedPassword(parseBooleanValue(name, value));
3480      }
3481      else if (name.equalsIgnoreCase("skip-password-validation") ||
3482           name.equalsIgnoreCase("skip-password-validators") ||
3483           name.equalsIgnoreCase("skip-validation") ||
3484           name.equalsIgnoreCase("skip-validators") ||
3485           name.equalsIgnoreCase("skipPasswordValidation") ||
3486           name.equalsIgnoreCase("skipPasswordValidators") ||
3487           name.equalsIgnoreCase("skipValidation") ||
3488           name.equalsIgnoreCase("skipValidators"))
3489      {
3490        properties.setSkipPasswordValidation(parseBooleanValue(name, value));
3491      }
3492      else if (name.equalsIgnoreCase("ignore-password-history") ||
3493           name.equalsIgnoreCase("skip-password-history") ||
3494           name.equalsIgnoreCase("ignore-history") ||
3495           name.equalsIgnoreCase("skip-history") ||
3496           name.equalsIgnoreCase("ignorePasswordHistory") ||
3497           name.equalsIgnoreCase("skipPasswordHistory") ||
3498           name.equalsIgnoreCase("ignoreHistory") ||
3499           name.equalsIgnoreCase("skipHistory"))
3500      {
3501        properties.setIgnorePasswordHistory(parseBooleanValue(name, value));
3502      }
3503      else if (name.equalsIgnoreCase("ignore-minimum-password-age") ||
3504           name.equalsIgnoreCase("ignore-min-password-age") ||
3505           name.equalsIgnoreCase("ignore-password-age") ||
3506           name.equalsIgnoreCase("skip-minimum-password-age") ||
3507           name.equalsIgnoreCase("skip-min-password-age") ||
3508           name.equalsIgnoreCase("skip-password-age") ||
3509           name.equalsIgnoreCase("ignoreMinimumPasswordAge") ||
3510           name.equalsIgnoreCase("ignoreMinPasswordAge") ||
3511           name.equalsIgnoreCase("ignorePasswordAge") ||
3512           name.equalsIgnoreCase("skipMinimumPasswordAge") ||
3513           name.equalsIgnoreCase("skipMinPasswordAge") ||
3514           name.equalsIgnoreCase("skipPasswordAge"))
3515      {
3516        properties.setIgnoreMinimumPasswordAge(parseBooleanValue(name, value));
3517      }
3518      else if (name.equalsIgnoreCase("password-storage-scheme") ||
3519           name.equalsIgnoreCase("password-scheme") ||
3520           name.equalsIgnoreCase("storage-scheme") ||
3521           name.equalsIgnoreCase("scheme") ||
3522           name.equalsIgnoreCase("passwordStorageScheme") ||
3523           name.equalsIgnoreCase("passwordScheme") ||
3524           name.equalsIgnoreCase("storageScheme"))
3525      {
3526        properties.setPasswordStorageScheme(value);
3527      }
3528      else if (name.equalsIgnoreCase("must-change-password") ||
3529         name.equalsIgnoreCase("mustChangePassword"))
3530      {
3531        properties.setMustChangePassword(parseBooleanValue(name, value));
3532      }
3533    }
3534
3535    return new PasswordUpdateBehaviorRequestControl(properties, true);
3536  }
3537
3538
3539
3540  /**
3541   * Parses the provided value as the Boolean value for a password update
3542   * behavior property.
3543   *
3544   * @param  name   The name of the password update behavior property being
3545   *                parsed.
3546   * @param  value  The value to be parsed.
3547   *
3548   * @return  The Boolean value that was parsed.
3549   *
3550   * @throws  LDAPException  If the provided value cannot be parsed as a
3551   *                         Boolean value.
3552   */
3553  private static boolean parseBooleanValue(@NotNull final String name,
3554                                           @NotNull final String value)
3555          throws LDAPException
3556  {
3557    if (value.equalsIgnoreCase("true") ||
3558         value.equalsIgnoreCase("t") ||
3559         value.equalsIgnoreCase("yes") ||
3560         value.equalsIgnoreCase("y") ||
3561         value.equalsIgnoreCase("1"))
3562    {
3563      return true;
3564    }
3565    else if (value.equalsIgnoreCase("false") ||
3566         value.equalsIgnoreCase("f") ||
3567         value.equalsIgnoreCase("no") ||
3568         value.equalsIgnoreCase("n") ||
3569         value.equalsIgnoreCase("0"))
3570    {
3571      return false;
3572    }
3573    else
3574    {
3575      throw new LDAPException(ResultCode.PARAM_ERROR,
3576           ERR_LDAPMODIFY_INVALID_PW_UPDATE_BOOLEAN_VALUE.get(value, name));
3577    }
3578  }
3579
3580
3581
3582  /**
3583   * Performs the appropriate processing for an LDIF add change record.
3584   *
3585   * @param  changeRecord         The LDIF add change record to process.
3586   * @param  controls             The set of controls to include in the request.
3587   * @param  pool                 The connection pool to use to communicate with
3588   *                              the directory server.
3589   * @param  multiUpdateRequests  The list to which the request should be added
3590   *                              if it is to be processed as part of a
3591   *                              multi-update operation.  It may be
3592   *                              {@code null} if the operation should not be
3593   *                              processed via the multi-update operation.
3594   * @param  rejectWriter         The LDIF writer to use for recording
3595   *                              information about rejected changes.  It may be
3596   *                              {@code null} if no reject writer is
3597   *                              configured.
3598   *
3599   * @return  The result code obtained from processing.
3600   *
3601   * @throws  LDAPException  If the operation did not complete successfully
3602   *                         and processing should not continue.
3603   */
3604  @NotNull()
3605  private ResultCode doAdd(@NotNull final LDIFAddChangeRecord changeRecord,
3606               @NotNull final List<Control> controls,
3607               @NotNull final LDAPConnectionPool pool,
3608               @Nullable final List<LDAPRequest> multiUpdateRequests,
3609               @Nullable final LDIFWriter rejectWriter)
3610          throws LDAPException
3611  {
3612    // Create the add request to process.
3613    final AddRequest addRequest = changeRecord.toAddRequest(true);
3614    for (final Control c : controls)
3615    {
3616      addRequest.addControl(c);
3617    }
3618
3619
3620    // If we should provide support for undelete operations and the entry
3621    // includes the ds-undelete-from-dn attribute, then add the undelete request
3622    // control.
3623    if (allowUndelete.isPresent() &&
3624        addRequest.hasAttribute(ATTR_UNDELETE_FROM_DN))
3625    {
3626      addRequest.addControl(new UndeleteRequestControl());
3627    }
3628
3629
3630    // If the entry to add includes a password, then add a password validation
3631    // details request control if appropriate.
3632    if (passwordValidationDetails.isPresent())
3633    {
3634      final Entry entryToAdd = addRequest.toEntry();
3635      if ((! entryToAdd.getAttributesWithOptions(ATTR_USER_PASSWORD,
3636                  null).isEmpty()) ||
3637          (! entryToAdd.getAttributesWithOptions(ATTR_AUTH_PASSWORD,
3638                  null).isEmpty()))
3639      {
3640        addRequest.addControl(new PasswordValidationDetailsRequestControl());
3641      }
3642    }
3643
3644
3645    // If the operation should be processed in a multi-update operation, then
3646    // just add the request to the list and return without doing anything else.
3647    if (multiUpdateErrorBehavior.isPresent())
3648    {
3649      multiUpdateRequests.add(addRequest);
3650      commentToOut(INFO_LDAPMODIFY_ADD_ADDED_TO_MULTI_UPDATE.get(
3651           addRequest.getDN()));
3652      return ResultCode.SUCCESS;
3653    }
3654
3655
3656    // If the --dryRun argument was provided, then we'll stop here.
3657    if (dryRun.isPresent())
3658    {
3659      commentToOut(INFO_LDAPMODIFY_DRY_RUN_ADD.get(addRequest.getDN(),
3660           dryRun.getIdentifierString()));
3661      return ResultCode.SUCCESS;
3662    }
3663
3664
3665    // Process the add operation and get the result.
3666    commentToOut(INFO_LDAPMODIFY_ADDING_ENTRY.get(addRequest.getDN()));
3667    if (verbose.isPresent())
3668    {
3669      for (final String ldifLine :
3670           addRequest.toLDIFChangeRecord().toLDIF(WRAP_COLUMN))
3671      {
3672        out(ldifLine);
3673      }
3674      out();
3675    }
3676
3677    LDAPResult addResult;
3678    try
3679    {
3680      addResult = pool.add(addRequest);
3681    }
3682    catch (final LDAPException le)
3683    {
3684      Debug.debugException(le);
3685      addResult = le.toLDAPResult();
3686    }
3687
3688    addResult = handleJSONEncodedResponseControls(addResult);
3689
3690
3691    // Display information about the result.
3692    displayResult(addResult, useTransaction.isPresent());
3693
3694
3695    // See if the add operation succeeded or failed.  If it failed, and we
3696    // should end all processing, then throw an exception.
3697    switch (addResult.getResultCode().intValue())
3698    {
3699      case ResultCode.SUCCESS_INT_VALUE:
3700      case ResultCode.NO_OPERATION_INT_VALUE:
3701        break;
3702
3703      case ResultCode.ASSERTION_FAILED_INT_VALUE:
3704        writeRejectedChange(rejectWriter,
3705             INFO_LDAPMODIFY_ASSERTION_FAILED.get(addRequest.getDN(),
3706                  String.valueOf(assertionFilter.getValue())),
3707             addRequest.toLDIFChangeRecord(), addResult);
3708        throw new LDAPException(addResult);
3709
3710      default:
3711        writeRejectedChange(rejectWriter, null, addRequest.toLDIFChangeRecord(),
3712             addResult);
3713        if (useTransaction.isPresent() || (! continueOnError.isPresent()))
3714        {
3715          throw new LDAPException(addResult);
3716        }
3717        break;
3718    }
3719
3720    return addResult.getResultCode();
3721  }
3722
3723
3724
3725  /**
3726   * Performs the appropriate processing for an LDIF delete change record.
3727   *
3728   * @param  changeRecord         The LDIF delete change record to process.
3729   * @param  controls             The set of controls to include in the request.
3730   * @param  pool                 The connection pool to use to communicate with
3731   *                              the directory server.
3732   * @param  multiUpdateRequests  The list to which the request should be added
3733   *                              if it is to be processed as part of a
3734   *                              multi-update operation.  It may be
3735   *                              {@code null} if the operation should not be
3736   *                              processed via the multi-update operation.
3737   * @param  rejectWriter         The LDIF writer to use for recording
3738   *                              information about rejected changes.  It may be
3739   *                              {@code null} if no reject writer is
3740   *                              configured.
3741   *
3742   * @return  The result code obtained from processing.
3743   *
3744   * @throws  LDAPException  If the operation did not complete successfully
3745   *                         and processing should not continue.
3746   */
3747  @NotNull()
3748  private ResultCode doDelete(
3749               @NotNull final LDIFDeleteChangeRecord changeRecord,
3750               @NotNull final List<Control> controls,
3751               @NotNull final LDAPConnectionPool pool,
3752               @Nullable final List<LDAPRequest> multiUpdateRequests,
3753               @Nullable final LDIFWriter rejectWriter)
3754          throws LDAPException
3755  {
3756    // If we should perform a client-side subtree delete, then do that
3757    // differently.
3758    if (clientSideSubtreeDelete.isPresent())
3759    {
3760      return doClientSideSubtreeDelete(changeRecord, controls, pool,
3761           rejectWriter);
3762    }
3763
3764
3765    // Create the delete request to process.
3766    final DeleteRequest deleteRequest = changeRecord.toDeleteRequest(true);
3767    for (final Control c : controls)
3768    {
3769      deleteRequest.addControl(c);
3770    }
3771
3772
3773    // If the operation should be processed in a multi-update operation, then
3774    // just add the request to the list and return without doing anything else.
3775    if (multiUpdateErrorBehavior.isPresent())
3776    {
3777      multiUpdateRequests.add(deleteRequest);
3778      commentToOut(INFO_LDAPMODIFY_DELETE_ADDED_TO_MULTI_UPDATE.get(
3779           deleteRequest.getDN()));
3780      return ResultCode.SUCCESS;
3781    }
3782
3783
3784    // If the --dryRun argument was provided, then we'll stop here.
3785    if (dryRun.isPresent())
3786    {
3787      commentToOut(INFO_LDAPMODIFY_DRY_RUN_DELETE.get(deleteRequest.getDN(),
3788           dryRun.getIdentifierString()));
3789      return ResultCode.SUCCESS;
3790    }
3791
3792
3793    // Process the delete operation and get the result.
3794    commentToOut(INFO_LDAPMODIFY_DELETING_ENTRY.get(deleteRequest.getDN()));
3795    if (verbose.isPresent())
3796    {
3797      for (final String ldifLine :
3798           deleteRequest.toLDIFChangeRecord().toLDIF(WRAP_COLUMN))
3799      {
3800        out(ldifLine);
3801      }
3802      out();
3803    }
3804
3805
3806    LDAPResult deleteResult;
3807    try
3808    {
3809      deleteResult = pool.delete(deleteRequest);
3810    }
3811    catch (final LDAPException le)
3812    {
3813      Debug.debugException(le);
3814      deleteResult = le.toLDAPResult();
3815    }
3816
3817    deleteResult = handleJSONEncodedResponseControls(deleteResult);
3818
3819
3820    // Display information about the result.
3821    displayResult(deleteResult, useTransaction.isPresent());
3822
3823
3824    // See if the delete operation succeeded or failed.  If it failed, and we
3825    // should end all processing, then throw an exception.
3826    switch (deleteResult.getResultCode().intValue())
3827    {
3828      case ResultCode.SUCCESS_INT_VALUE:
3829      case ResultCode.NO_OPERATION_INT_VALUE:
3830        break;
3831
3832      case ResultCode.ASSERTION_FAILED_INT_VALUE:
3833        writeRejectedChange(rejectWriter,
3834             INFO_LDAPMODIFY_ASSERTION_FAILED.get(deleteRequest.getDN(),
3835                  String.valueOf(assertionFilter.getValue())),
3836             deleteRequest.toLDIFChangeRecord(), deleteResult);
3837        throw new LDAPException(deleteResult);
3838
3839      default:
3840        writeRejectedChange(rejectWriter, null,
3841             deleteRequest.toLDIFChangeRecord(), deleteResult);
3842        if (useTransaction.isPresent() || (! continueOnError.isPresent()))
3843        {
3844          throw new LDAPException(deleteResult);
3845        }
3846        break;
3847    }
3848
3849    return deleteResult.getResultCode();
3850  }
3851
3852
3853
3854  /**
3855   * Performs the appropriate processing for an LDIF delete change record.
3856   *
3857   * @param  changeRecord  The LDIF delete change record to process.
3858   * @param  controls      The set of controls to include in the request.
3859   * @param  pool          The connection pool to use to communicate with the
3860   *                       directory server.
3861   * @param  rejectWriter  The LDIF writer to use for recording information
3862   *                       about rejected changes.  It may be {@code null} if no
3863   *                       reject writer is configured.
3864   *
3865   * @return  The result code obtained from processing.
3866   *
3867   * @throws  LDAPException  If the operation did not complete successfully
3868   *                         and processing should not continue.
3869   */
3870  @NotNull()
3871  private ResultCode doClientSideSubtreeDelete(
3872                          @NotNull final LDIFChangeRecord changeRecord,
3873                          @NotNull final List<Control> controls,
3874                          @NotNull final LDAPConnectionPool pool,
3875                          @Nullable final LDIFWriter rejectWriter)
3876          throws LDAPException
3877  {
3878    // Create the subtree deleter with the provided set of controls.  Make sure
3879    // to include any controls in the delete change record itself.
3880    final List<Control> additionalControls;
3881    if (changeRecord.getControls().isEmpty())
3882    {
3883      additionalControls = controls;
3884    }
3885    else
3886    {
3887      additionalControls = new ArrayList<>(controls.size() +
3888           changeRecord.getControls().size());
3889      additionalControls.addAll(changeRecord.getControls());
3890      additionalControls.addAll(controls);
3891    }
3892
3893    final SubtreeDeleter subtreeDeleter = new SubtreeDeleter();
3894    subtreeDeleter.setAdditionalDeleteControls(additionalControls);
3895
3896
3897    // Perform the subtree delete.
3898    commentToOut(INFO_LDAPMODIFY_CLIENT_SIDE_DELETING_SUBTREE.get(
3899         changeRecord.getDN()));
3900    final SubtreeDeleterResult subtreeDeleterResult =
3901         subtreeDeleter.delete(pool, changeRecord.getDN());
3902
3903
3904    // Evaluate the result of the subtree delete.
3905    LDAPResult finalResult;
3906    if (subtreeDeleterResult.completelySuccessful())
3907    {
3908      final long entriesDeleted = subtreeDeleterResult.getEntriesDeleted();
3909      if (entriesDeleted == 0L)
3910      {
3911        // This means that the base entry did not exist.  Even though the
3912        // subtree deleter returned a successful result, we'll use a final
3913        // result of "no such object".
3914        finalResult = new LDAPResult(-1, ResultCode.NO_SUCH_OBJECT,
3915             ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_SUCCEEDED_WITH_0_ENTRIES.get(
3916                  changeRecord.getDN()),
3917             null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
3918      }
3919      else if (entriesDeleted == 1L)
3920      {
3921        // This means the base entry existed (and we deleted it successfully),
3922        // but did not have any subordinates.
3923        finalResult = new LDAPResult(-1, ResultCode.SUCCESS,
3924             INFO_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_SUCCEEDED_WITH_1_ENTRY.get(
3925                  changeRecord.getDN()),
3926             null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
3927      }
3928      else
3929      {
3930        // This means that the base entry existed and had subordinates, and we
3931        // deleted all of them successfully.
3932        finalResult = new LDAPResult(-1, ResultCode.SUCCESS,
3933             INFO_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_SUCCEEDED_WITH_ENTRIES.get(
3934                  subtreeDeleterResult.getEntriesDeleted(),
3935                  changeRecord.getDN()),
3936             null, StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
3937      }
3938    }
3939    else
3940    {
3941      // If there was a search error, then display information about it.
3942      final SearchResult searchError = subtreeDeleterResult.getSearchError();
3943      if (searchError != null)
3944      {
3945        commentToErr(ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_SEARCH_ERROR.get());
3946        displayResult(searchError, false);
3947        err("#");
3948      }
3949
3950      final SortedMap<DN,LDAPResult> deleteErrors =
3951           subtreeDeleterResult.getDeleteErrorsDescendingMap();
3952      for (final Map.Entry<DN,LDAPResult> deleteError : deleteErrors.entrySet())
3953      {
3954        commentToErr(ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_ERROR.get(
3955             String.valueOf(deleteError.getKey())));
3956        displayResult(deleteError.getValue(), false);
3957        err("#");
3958      }
3959
3960      ResultCode resultCode = ResultCode.OTHER;
3961      final StringBuilder buffer = new StringBuilder();
3962      buffer.append(ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_FINAL_ERR_BASE.get());
3963      if (searchError != null)
3964      {
3965        resultCode = searchError.getResultCode();
3966        buffer.append("  ");
3967        buffer.append(
3968             ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_FINAL_SEARCH_ERR.get());
3969      }
3970
3971      if (! deleteErrors.isEmpty())
3972      {
3973        resultCode = deleteErrors.values().iterator().next().getResultCode();
3974        buffer.append("  ");
3975        final int numDeleteErrors = deleteErrors.size();
3976        if (numDeleteErrors == 1)
3977        {
3978          buffer.append(
3979               ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_FINAL_DEL_ERR_COUNT_1.get());
3980        }
3981        else
3982        {
3983          buffer.append(
3984               ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_FINAL_DEL_ERR_COUNT.get(
3985                    numDeleteErrors));
3986        }
3987      }
3988
3989      buffer.append("  ");
3990      final long deletedCount = subtreeDeleterResult.getEntriesDeleted();
3991      if (deletedCount == 1L)
3992      {
3993        buffer.append(
3994             ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_FINAL_DEL_COUNT_1.get());
3995      }
3996      else
3997      {
3998        buffer.append(ERR_LDAPMODIFY_CLIENT_SIDE_SUB_DEL_FINAL_DEL_COUNT.get(
3999             deletedCount));
4000      }
4001
4002      finalResult = new LDAPResult(-1, resultCode, buffer.toString(), null,
4003           StaticUtils.NO_STRINGS, StaticUtils.NO_CONTROLS);
4004    }
4005
4006    finalResult = handleJSONEncodedResponseControls(finalResult);
4007
4008
4009    // Display information about the final result.
4010    displayResult(finalResult, useTransaction.isPresent());
4011
4012
4013    // See if the delete operation succeeded or failed.  If it failed, and we
4014    // should end all processing, then throw an exception.
4015    switch (finalResult.getResultCode().intValue())
4016    {
4017      case ResultCode.SUCCESS_INT_VALUE:
4018      case ResultCode.NO_OPERATION_INT_VALUE:
4019        break;
4020
4021      default:
4022        writeRejectedChange(rejectWriter, null, changeRecord, finalResult);
4023        if (! continueOnError.isPresent())
4024        {
4025          throw new LDAPException(finalResult);
4026        }
4027        break;
4028    }
4029
4030    return finalResult.getResultCode();
4031  }
4032
4033
4034
4035  /**
4036   * Performs the appropriate processing for an LDIF modify change record.
4037   *
4038   * @param  changeRecord         The LDIF modify change record to process.
4039   * @param  controls             The set of controls to include in the request.
4040   * @param  pool                 The connection pool to use to communicate with
4041   *                              the directory server.
4042   * @param  multiUpdateRequests  The list to which the request should be added
4043   *                              if it is to be processed as part of a
4044   *                              multi-update operation.  It may be
4045   *                              {@code null} if the operation should not be
4046   *                              processed via the multi-update operation.
4047   * @param  rejectWriter         The LDIF writer to use for recording
4048   *                              information about rejected changes.  It may be
4049   *                              {@code null} if no reject writer is
4050   *                              configured.
4051   *
4052   * @return  The result code obtained from processing.
4053   *
4054   * @throws  LDAPException  If the operation did not complete successfully
4055   *                         and processing should not continue.
4056   */
4057  @NotNull()
4058  ResultCode doModify(@NotNull final LDIFModifyChangeRecord changeRecord,
4059                      @NotNull final List<Control> controls,
4060                      @NotNull final LDAPConnectionPool pool,
4061                      @Nullable final List<LDAPRequest> multiUpdateRequests,
4062                      @Nullable final LDIFWriter rejectWriter)
4063             throws LDAPException
4064  {
4065    // Create the modify request to process.
4066    final ModifyRequest modifyRequest = changeRecord.toModifyRequest(true);
4067    for (final Control c : controls)
4068    {
4069      modifyRequest.addControl(c);
4070    }
4071
4072
4073    // If the modify request includes a password change, then add any controls
4074    // that are specific to that.
4075    if (retireCurrentPassword.isPresent() || purgeCurrentPassword.isPresent() ||
4076        passwordValidationDetails.isPresent())
4077    {
4078      for (final Modification m : modifyRequest.getModifications())
4079      {
4080        final String baseName = m.getAttribute().getBaseName();
4081        if (baseName.equalsIgnoreCase(ATTR_USER_PASSWORD) ||
4082            baseName.equalsIgnoreCase(ATTR_AUTH_PASSWORD))
4083        {
4084          if (retireCurrentPassword.isPresent())
4085          {
4086            modifyRequest.addControl(new RetirePasswordRequestControl(false));
4087          }
4088          else if (purgeCurrentPassword.isPresent())
4089          {
4090            modifyRequest.addControl(new PurgePasswordRequestControl(false));
4091          }
4092
4093          if (passwordValidationDetails.isPresent())
4094          {
4095            modifyRequest.addControl(
4096                 new PasswordValidationDetailsRequestControl());
4097          }
4098
4099          break;
4100        }
4101      }
4102    }
4103
4104
4105    // If the operation should be processed in a multi-update operation, then
4106    // just add the request to the list and return without doing anything else.
4107    if (multiUpdateErrorBehavior.isPresent())
4108    {
4109      multiUpdateRequests.add(modifyRequest);
4110      commentToOut(INFO_LDAPMODIFY_MODIFY_ADDED_TO_MULTI_UPDATE.get(
4111           modifyRequest.getDN()));
4112      return ResultCode.SUCCESS;
4113    }
4114
4115
4116    // If the --dryRun argument was provided, then we'll stop here.
4117    if (dryRun.isPresent())
4118    {
4119      commentToOut(INFO_LDAPMODIFY_DRY_RUN_MODIFY.get(modifyRequest.getDN(),
4120           dryRun.getIdentifierString()));
4121      return ResultCode.SUCCESS;
4122    }
4123
4124
4125    // Process the modify operation and get the result.
4126    commentToOut(INFO_LDAPMODIFY_MODIFYING_ENTRY.get(modifyRequest.getDN()));
4127    if (verbose.isPresent())
4128    {
4129      for (final String ldifLine :
4130           modifyRequest.toLDIFChangeRecord().toLDIF(WRAP_COLUMN))
4131      {
4132        out(ldifLine);
4133      }
4134      out();
4135    }
4136
4137
4138    LDAPResult modifyResult;
4139    try
4140    {
4141      modifyResult = pool.modify(modifyRequest);
4142    }
4143    catch (final LDAPException le)
4144    {
4145      Debug.debugException(le);
4146      modifyResult = le.toLDAPResult();
4147    }
4148
4149    modifyResult = handleJSONEncodedResponseControls(modifyResult);
4150
4151
4152    // Display information about the result.
4153    displayResult(modifyResult, useTransaction.isPresent());
4154
4155
4156    // See if the modify operation succeeded or failed.  If it failed, and we
4157    // should end all processing, then throw an exception.
4158    switch (modifyResult.getResultCode().intValue())
4159    {
4160      case ResultCode.SUCCESS_INT_VALUE:
4161      case ResultCode.NO_OPERATION_INT_VALUE:
4162        break;
4163
4164      case ResultCode.ASSERTION_FAILED_INT_VALUE:
4165        writeRejectedChange(rejectWriter,
4166             INFO_LDAPMODIFY_ASSERTION_FAILED.get(modifyRequest.getDN(),
4167                  String.valueOf(assertionFilter.getValue())),
4168             modifyRequest.toLDIFChangeRecord(), modifyResult);
4169        throw new LDAPException(modifyResult);
4170
4171      default:
4172        writeRejectedChange(rejectWriter, null,
4173             modifyRequest.toLDIFChangeRecord(), modifyResult);
4174        if (useTransaction.isPresent() || (! continueOnError.isPresent()))
4175        {
4176          throw new LDAPException(modifyResult);
4177        }
4178        break;
4179    }
4180
4181    return modifyResult.getResultCode();
4182  }
4183
4184
4185
4186  /**
4187   * Performs the appropriate processing for an LDIF modify DN change record.
4188   *
4189   * @param  changeRecord         The LDIF modify DN change record to process.
4190   * @param  controls             The set of controls to include in the request.
4191   * @param  pool                 The connection pool to use to communicate with
4192   *                              the directory server.
4193   * @param  multiUpdateRequests  The list to which the request should be added
4194   *                              if it is to be processed as part of a
4195   *                              multi-update operation.  It may be
4196   *                              {@code null} if the operation should not be
4197   *                              processed via the multi-update operation.
4198   * @param  rejectWriter         The LDIF writer to use for recording
4199   *                              information about rejected changes.  It may be
4200   *                              {@code null} if no reject writer is
4201   *                              configured.
4202   *
4203   * @return  The result code obtained from processing.
4204   *
4205   * @throws  LDAPException  If the operation did not complete successfully
4206   *                         and processing should not continue.
4207   */
4208  @NotNull()
4209  private ResultCode doModifyDN(
4210               @NotNull final LDIFModifyDNChangeRecord changeRecord,
4211               @NotNull final List<Control> controls,
4212               @NotNull final LDAPConnectionPool pool,
4213               @Nullable final List<LDAPRequest> multiUpdateRequests,
4214               @Nullable final LDIFWriter rejectWriter)
4215          throws LDAPException
4216  {
4217    // Create the modify DN request to process.
4218    final ModifyDNRequest modifyDNRequest =
4219         changeRecord.toModifyDNRequest(true);
4220    for (final Control c : controls)
4221    {
4222      modifyDNRequest.addControl(c);
4223    }
4224
4225
4226    // If the operation should be processed in a multi-update operation, then
4227    // just add the request to the list and return without doing anything else.
4228    if (multiUpdateErrorBehavior.isPresent())
4229    {
4230      multiUpdateRequests.add(modifyDNRequest);
4231      commentToOut(INFO_LDAPMODIFY_MODIFY_DN_ADDED_TO_MULTI_UPDATE.get(
4232           modifyDNRequest.getDN()));
4233      return ResultCode.SUCCESS;
4234    }
4235
4236
4237    // Try to determine the new DN that the entry will have after the operation.
4238    DN newDN = null;
4239    try
4240    {
4241      newDN = changeRecord.getNewDN();
4242    }
4243    catch (final Exception e)
4244    {
4245      Debug.debugException(e);
4246
4247      // This should only happen if the provided DN, new RDN, or new superior DN
4248      // was malformed.  Although we could reject the operation now, we'll go
4249      // ahead and send the request to the server in case it has some special
4250      // handling for the DN.
4251    }
4252
4253
4254    // If the --dryRun argument was provided, then we'll stop here.
4255    if (dryRun.isPresent())
4256    {
4257      if (modifyDNRequest.getNewSuperiorDN() == null)
4258      {
4259        if (newDN == null)
4260        {
4261          commentToOut(INFO_LDAPMODIFY_DRY_RUN_RENAME.get(
4262               modifyDNRequest.getDN(), dryRun.getIdentifierString()));
4263        }
4264        else
4265        {
4266          commentToOut(INFO_LDAPMODIFY_DRY_RUN_RENAME_TO.get(
4267               modifyDNRequest.getDN(), newDN.toString(),
4268               dryRun.getIdentifierString()));
4269        }
4270      }
4271      else
4272      {
4273        if (newDN == null)
4274        {
4275          commentToOut(INFO_LDAPMODIFY_DRY_RUN_MOVE.get(
4276               modifyDNRequest.getDN(), dryRun.getIdentifierString()));
4277        }
4278        else
4279        {
4280          commentToOut(INFO_LDAPMODIFY_DRY_RUN_MOVE_TO.get(
4281               modifyDNRequest.getDN(), newDN.toString(),
4282               dryRun.getIdentifierString()));
4283        }
4284      }
4285      return ResultCode.SUCCESS;
4286    }
4287
4288
4289    // Process the modify DN operation and get the result.
4290    final String currentDN = modifyDNRequest.getDN();
4291    if (modifyDNRequest.getNewSuperiorDN() == null)
4292    {
4293      if (newDN == null)
4294      {
4295        commentToOut(INFO_LDAPMODIFY_MOVING_ENTRY.get(currentDN));
4296      }
4297      else
4298      {
4299        commentToOut(INFO_LDAPMODIFY_MOVING_ENTRY_TO.get(currentDN,
4300             newDN.toString()));
4301      }
4302    }
4303    else
4304    {
4305      if (newDN == null)
4306      {
4307        commentToOut(INFO_LDAPMODIFY_RENAMING_ENTRY.get(currentDN));
4308      }
4309      else
4310      {
4311        commentToOut(INFO_LDAPMODIFY_RENAMING_ENTRY_TO.get(currentDN,
4312             newDN.toString()));
4313      }
4314    }
4315
4316    if (verbose.isPresent())
4317    {
4318      for (final String ldifLine :
4319           modifyDNRequest.toLDIFChangeRecord().toLDIF(WRAP_COLUMN))
4320      {
4321        out(ldifLine);
4322      }
4323      out();
4324    }
4325
4326
4327    LDAPResult modifyDNResult;
4328    try
4329    {
4330      modifyDNResult = pool.modifyDN(modifyDNRequest);
4331    }
4332    catch (final LDAPException le)
4333    {
4334      Debug.debugException(le);
4335      modifyDNResult = le.toLDAPResult();
4336    }
4337
4338    modifyDNResult = handleJSONEncodedResponseControls(modifyDNResult);
4339
4340
4341    // Display information about the result.
4342    displayResult(modifyDNResult, useTransaction.isPresent());
4343
4344
4345    // See if the modify DN operation succeeded or failed.  If it failed, and we
4346    // should end all processing, then throw an exception.
4347    switch (modifyDNResult.getResultCode().intValue())
4348    {
4349      case ResultCode.SUCCESS_INT_VALUE:
4350      case ResultCode.NO_OPERATION_INT_VALUE:
4351        break;
4352
4353      case ResultCode.ASSERTION_FAILED_INT_VALUE:
4354        writeRejectedChange(rejectWriter,
4355             INFO_LDAPMODIFY_ASSERTION_FAILED.get(modifyDNRequest.getDN(),
4356                  String.valueOf(assertionFilter.getValue())),
4357             modifyDNRequest.toLDIFChangeRecord(), modifyDNResult);
4358        throw new LDAPException(modifyDNResult);
4359
4360      default:
4361        writeRejectedChange(rejectWriter, null,
4362             modifyDNRequest.toLDIFChangeRecord(), modifyDNResult);
4363        if (useTransaction.isPresent() || (! continueOnError.isPresent()))
4364        {
4365          throw new LDAPException(modifyDNResult);
4366        }
4367        break;
4368    }
4369
4370    return modifyDNResult.getResultCode();
4371  }
4372
4373
4374
4375  /**
4376   * Displays information about the provided result, including special
4377   * processing for a number of supported response controls.
4378   *
4379   * @param  result         The result to examine.
4380   * @param  inTransaction  Indicates whether the operation is part of a
4381   *                        transaction.
4382   */
4383  private void displayResult(@NotNull final LDAPResult result,
4384                             final boolean inTransaction)
4385  {
4386    final ArrayList<String> resultLines = new ArrayList<>(10);
4387    ResultUtils.formatResult(resultLines, result, true, inTransaction, 0,
4388         WRAP_COLUMN);
4389
4390    if (result.getResultCode() == ResultCode.SUCCESS)
4391    {
4392      for (final String line : resultLines)
4393      {
4394        out(line);
4395      }
4396      out();
4397    }
4398    else
4399    {
4400      for (final String line : resultLines)
4401      {
4402        err(line);
4403      }
4404      err();
4405    }
4406  }
4407
4408
4409
4410  /**
4411   * Writes a line-wrapped, commented version of the provided message to
4412   * standard output.
4413   *
4414   * @param  message  The message to be written.
4415   */
4416  private void commentToOut(@NotNull final String message)
4417  {
4418    for (final String line : StaticUtils.wrapLine(message, WRAP_COLUMN - 2))
4419    {
4420      out("# ", line);
4421    }
4422  }
4423
4424
4425
4426  /**
4427   * Writes a line-wrapped, commented version of the provided message to
4428   * standard error.
4429   *
4430   * @param  message  The message to be written.
4431   */
4432  private void commentToErr(@NotNull final String message)
4433  {
4434    for (final String line : StaticUtils.wrapLine(message, WRAP_COLUMN - 2))
4435    {
4436      err("# ", line);
4437    }
4438  }
4439
4440
4441
4442  /**
4443   * Writes information about the rejected change to the reject writer.
4444   *
4445   * @param  writer        The LDIF writer to which the information should be
4446   *                       written.  It may be {@code null} if no reject file is
4447   *                       configured.
4448   * @param  comment       The comment to include before the change record, in
4449   *                       addition to the comment generated from the provided
4450   *                       LDAP result.  It may be {@code null} if no additional
4451   *                       comment should be included.
4452   * @param  changeRecord  The LDIF change record to be written.  It must not
4453   *                       be {@code null}.
4454   * @param  ldapResult    The LDAP result for the failed operation.  It must
4455   *                       not be {@code null}.
4456   */
4457  private void writeRejectedChange(@Nullable final LDIFWriter writer,
4458                                   @Nullable final String comment,
4459                                   @NotNull final LDIFChangeRecord changeRecord,
4460                                   @NotNull final LDAPResult ldapResult)
4461  {
4462    if (writer == null)
4463    {
4464      return;
4465    }
4466
4467
4468    final StringBuilder buffer = new StringBuilder();
4469    if (comment != null)
4470    {
4471      buffer.append(comment);
4472      buffer.append(StaticUtils.EOL);
4473      buffer.append(StaticUtils.EOL);
4474    }
4475
4476    final ArrayList<String> resultLines = new ArrayList<>(10);
4477    ResultUtils.formatResult(resultLines, ldapResult, false, false, 0, 0);
4478    for (final String resultLine : resultLines)
4479    {
4480      buffer.append(resultLine);
4481      buffer.append(StaticUtils.EOL);
4482    }
4483
4484    writeRejectedChange(writer, buffer.toString(), changeRecord);
4485  }
4486
4487
4488
4489  /**
4490   * Writes information about the rejected change to the reject writer.
4491   *
4492   * @param  writer        The LDIF writer to which the information should be
4493   *                       written.  It may be {@code null} if no reject file is
4494   *                       configured.
4495   * @param  comment       The comment to include before the change record.  It
4496   *                       may be {@code null} if no comment should be included.
4497   * @param  changeRecord  The LDIF change record to be written.  It may be
4498   *                       {@code null} if only a comment should be written.
4499   */
4500  void writeRejectedChange(@Nullable final LDIFWriter writer,
4501                           @Nullable final String comment,
4502                           @Nullable final LDIFChangeRecord changeRecord)
4503  {
4504    if (writer == null)
4505    {
4506      return;
4507    }
4508
4509    if (rejectWritten.compareAndSet(false, true))
4510    {
4511      try
4512      {
4513        writer.writeVersionHeader();
4514      }
4515      catch (final Exception e)
4516      {
4517        Debug.debugException(e);
4518      }
4519    }
4520
4521    try
4522    {
4523      if (comment != null)
4524      {
4525        writer.writeComment(comment, true, false);
4526      }
4527
4528      if (changeRecord != null)
4529      {
4530        writer.writeChangeRecord(changeRecord);
4531      }
4532    }
4533    catch (final Exception e)
4534    {
4535      Debug.debugException(e);
4536
4537      commentToErr(ERR_LDAPMODIFY_UNABLE_TO_WRITE_REJECTED_CHANGE.get(
4538           rejectFile.getValue().getAbsolutePath(),
4539           StaticUtils.getExceptionMessage(e)));
4540    }
4541  }
4542
4543
4544
4545  /**
4546   * {@inheritDoc}
4547   */
4548  @Override()
4549  public void handleUnsolicitedNotification(
4550                   @NotNull final LDAPConnection connection,
4551                   @NotNull final ExtendedResult notification)
4552  {
4553    final ArrayList<String> lines = new ArrayList<>(10);
4554    ResultUtils.formatUnsolicitedNotification(lines, notification, true, 0,
4555         WRAP_COLUMN);
4556    for (final String line : lines)
4557    {
4558      err(line);
4559    }
4560    err();
4561  }
4562
4563
4564
4565  /**
4566   * Examines the provided LDAP result to see if it includes a JSONf-formatted
4567   * response control.  If so, then its embedded controls will be extracted and
4568   * a new LDAP result will be returned with those extracted controls instead
4569   * of the JSON-formatted response control.  Otherwise, the provided LDAP
4570   * result will be returned.
4571   *
4572   * @param  ldapResult  The LDAP result to be handled.  It must not be
4573   *                     {@code null}.
4574   *
4575   * @return  A new LDAP result with the controls extracted from a
4576   *          JSON-formatted response control, or the original LDAP result if
4577   *          it did not include a JSON-formatted response control.
4578   */
4579  @NotNull()
4580  static LDAPResult handleJSONEncodedResponseControls(
4581              @NotNull final LDAPResult ldapResult)
4582  {
4583    try
4584    {
4585      final JSONFormattedResponseControl jsonFormattedResponseControl =
4586           JSONFormattedResponseControl.get(ldapResult);
4587      if (jsonFormattedResponseControl == null)
4588      {
4589        return ldapResult;
4590      }
4591
4592      final JSONFormattedControlDecodeBehavior decodeBehavior =
4593           new JSONFormattedControlDecodeBehavior();
4594      decodeBehavior.setThrowOnUnparsableObject(false);
4595      decodeBehavior.setThrowOnInvalidCriticalControl(false);
4596      decodeBehavior.setThrowOnInvalidNonCriticalControl(false);
4597      decodeBehavior.setThrowOnInvalidNonCriticalControl(false);
4598      decodeBehavior.setAllowEmbeddedJSONFormattedControl(true);
4599      decodeBehavior.setStrict(false);
4600
4601      final List<Control> decodedControls =
4602           jsonFormattedResponseControl.decodeEmbeddedControls(
4603                decodeBehavior, null);
4604
4605      return new LDAPResult(ldapResult.getMessageID(),
4606           ldapResult.getResultCode(),
4607           ldapResult.getDiagnosticMessage(),
4608           ldapResult.getMatchedDN(),
4609           ldapResult.getReferralURLs(),
4610           StaticUtils.toArray(decodedControls, Control.class));
4611    }
4612    catch (final LDAPException e)
4613    {
4614      Debug.debugException(e);
4615      return ldapResult;
4616    }
4617  }
4618
4619
4620
4621  /**
4622   * {@inheritDoc}
4623   */
4624  @Override()
4625  @NotNull()
4626  public LinkedHashMap<String[],String> getExampleUsages()
4627  {
4628    final LinkedHashMap<String[],String> examples =
4629         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
4630
4631    final String[] args1 =
4632    {
4633      "--hostname", "ldap.example.com",
4634      "--port", "389",
4635      "--bindDN", "uid=admin,dc=example,dc=com",
4636      "--bindPassword", "password",
4637      "--defaultAdd"
4638    };
4639    examples.put(args1, INFO_LDAPMODIFY_EXAMPLE_1.get());
4640
4641    final String[] args2 =
4642    {
4643      "--hostname", "ds1.example.com",
4644      "--port", "636",
4645      "--hostname", "ds2.example.com",
4646      "--port", "636",
4647      "--useSSL",
4648      "--bindDN", "uid=admin,dc=example,dc=com",
4649      "--bindPassword", "password",
4650      "--ldifFile", "changes.ldif",
4651      "--modifyEntriesMatchingFilter", "(objectClass=person)",
4652      "--searchPageSize", "100"
4653    };
4654    examples.put(args2, INFO_LDAPMODIFY_EXAMPLE_2.get());
4655
4656    return examples;
4657  }
4658}