001/*
002 * Copyright 2008-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2008-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) 2008-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.examples;
037
038
039
040import java.io.OutputStream;
041import java.io.Serializable;
042import java.text.ParseException;
043import java.util.Iterator;
044import java.util.LinkedHashMap;
045import java.util.List;
046
047import com.unboundid.ldap.sdk.CompareRequest;
048import com.unboundid.ldap.sdk.CompareResult;
049import com.unboundid.ldap.sdk.Control;
050import com.unboundid.ldap.sdk.DN;
051import com.unboundid.ldap.sdk.LDAPConnection;
052import com.unboundid.ldap.sdk.LDAPException;
053import com.unboundid.ldap.sdk.ResultCode;
054import com.unboundid.ldap.sdk.Version;
055import com.unboundid.util.Base64;
056import com.unboundid.util.Debug;
057import com.unboundid.util.LDAPCommandLineTool;
058import com.unboundid.util.NotNull;
059import com.unboundid.util.Nullable;
060import com.unboundid.util.StaticUtils;
061import com.unboundid.util.ThreadSafety;
062import com.unboundid.util.ThreadSafetyLevel;
063import com.unboundid.util.args.ArgumentException;
064import com.unboundid.util.args.ArgumentParser;
065import com.unboundid.util.args.ControlArgument;
066
067
068
069/**
070 * This class provides a simple tool that can be used to perform compare
071 * operations in an LDAP directory server.  All of the necessary information is
072 * provided using command line arguments.    Supported arguments include those
073 * allowed by the {@link LDAPCommandLineTool} class.  In addition, a set of at
074 * least two unnamed trailing arguments must be given.  The first argument
075 * should be a string containing the name of the target attribute followed by a
076 * colon and the assertion value to use for that attribute (e.g.,
077 * "cn:john doe").  Alternately, the attribute name may be followed by two
078 * colons and the base64-encoded representation of the assertion value
079 * (e.g., "cn::  am9obiBkb2U=").  Any subsequent trailing arguments will be the
080 * DN(s) of entries in which to perform the compare operation(s).
081 * <BR><BR>
082 * Some of the APIs demonstrated by this example include:
083 * <UL>
084 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
085 *       package)</LI>
086 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
087 *       package)</LI>
088 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
089 *       package)</LI>
090 * </UL>
091 */
092@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
093public final class LDAPCompare
094       extends LDAPCommandLineTool
095       implements Serializable
096{
097  /**
098   * The serial version UID for this serializable class.
099   */
100  private static final long serialVersionUID = 719069383330181184L;
101
102
103
104  // The argument parser for this tool.
105  @Nullable private ArgumentParser parser;
106
107  // The argument used to specify any bind controls that should be used.
108  @Nullable private ControlArgument bindControls;
109
110  // The argument used to specify any compare controls that should be used.
111  @Nullable private ControlArgument compareControls;
112
113
114
115  /**
116   * Parse the provided command line arguments and make the appropriate set of
117   * changes.
118   *
119   * @param  args  The command line arguments provided to this program.
120   */
121  public static void main(@NotNull final String[] args)
122  {
123    final ResultCode resultCode = main(args, System.out, System.err);
124    if (resultCode != ResultCode.SUCCESS)
125    {
126      System.exit(resultCode.intValue());
127    }
128  }
129
130
131
132  /**
133   * Parse the provided command line arguments and make the appropriate set of
134   * changes.
135   *
136   * @param  args       The command line arguments provided to this program.
137   * @param  outStream  The output stream to which standard out should be
138   *                    written.  It may be {@code null} if output should be
139   *                    suppressed.
140   * @param  errStream  The output stream to which standard error should be
141   *                    written.  It may be {@code null} if error messages
142   *                    should be suppressed.
143   *
144   * @return  A result code indicating whether the processing was successful.
145   */
146  @NotNull()
147  public static ResultCode main(@NotNull final String[] args,
148                                @Nullable final OutputStream outStream,
149                                @Nullable final OutputStream errStream)
150  {
151    final LDAPCompare ldapCompare = new LDAPCompare(outStream, errStream);
152    return ldapCompare.runTool(args);
153  }
154
155
156
157  /**
158   * Creates a new instance of this tool.
159   *
160   * @param  outStream  The output stream to which standard out should be
161   *                    written.  It may be {@code null} if output should be
162   *                    suppressed.
163   * @param  errStream  The output stream to which standard error should be
164   *                    written.  It may be {@code null} if error messages
165   *                    should be suppressed.
166   */
167  public LDAPCompare(@Nullable final OutputStream outStream,
168                     @Nullable final OutputStream errStream)
169  {
170    super(outStream, errStream);
171  }
172
173
174
175  /**
176   * Retrieves the name for this tool.
177   *
178   * @return  The name for this tool.
179   */
180  @Override()
181  @NotNull()
182  public String getToolName()
183  {
184    return "ldapcompare";
185  }
186
187
188
189  /**
190   * Retrieves the description for this tool.
191   *
192   * @return  The description for this tool.
193   */
194  @Override()
195  @NotNull()
196  public String getToolDescription()
197  {
198    return "Perform LDAP compare operations in an LDAP directory server.";
199  }
200
201
202
203  /**
204   * Retrieves the version string for this tool.
205   *
206   * @return  The version string for this tool.
207   */
208  @Override()
209  @NotNull()
210  public String getToolVersion()
211  {
212    return Version.NUMERIC_VERSION_STRING;
213  }
214
215
216
217  /**
218   * Retrieves the minimum number of unnamed trailing arguments that are
219   * required.
220   *
221   * @return  Two, to indicate that at least two trailing arguments
222   *          (representing the attribute value assertion and at least one entry
223   *          DN) must be provided.
224   */
225  @Override()
226  public int getMinTrailingArguments()
227  {
228    return 2;
229  }
230
231
232
233  /**
234   * Retrieves the maximum number of unnamed trailing arguments that are
235   * allowed.
236   *
237   * @return  A negative value to indicate that any number of trailing arguments
238   *          may be provided.
239   */
240  @Override()
241  public int getMaxTrailingArguments()
242  {
243    return -1;
244  }
245
246
247
248  /**
249   * Retrieves a placeholder string that may be used to indicate what kinds of
250   * trailing arguments are allowed.
251   *
252   * @return  A placeholder string that may be used to indicate what kinds of
253   *          trailing arguments are allowed.
254   */
255  @Override()
256  @NotNull()
257  public String getTrailingArgumentsPlaceholder()
258  {
259    return "attr:value dn1 [dn2 [dn3 [...]]]";
260  }
261
262
263
264  /**
265   * Indicates whether this tool should provide support for an interactive mode,
266   * in which the tool offers a mode in which the arguments can be provided in
267   * a text-driven menu rather than requiring them to be given on the command
268   * line.  If interactive mode is supported, it may be invoked using the
269   * "--interactive" argument.  Alternately, if interactive mode is supported
270   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
271   * interactive mode may be invoked by simply launching the tool without any
272   * arguments.
273   *
274   * @return  {@code true} if this tool supports interactive mode, or
275   *          {@code false} if not.
276   */
277  @Override()
278  public boolean supportsInteractiveMode()
279  {
280    return true;
281  }
282
283
284
285  /**
286   * Indicates whether this tool defaults to launching in interactive mode if
287   * the tool is invoked without any command-line arguments.  This will only be
288   * used if {@link #supportsInteractiveMode()} returns {@code true}.
289   *
290   * @return  {@code true} if this tool defaults to using interactive mode if
291   *          launched without any command-line arguments, or {@code false} if
292   *          not.
293   */
294  @Override()
295  public boolean defaultsToInteractiveMode()
296  {
297    return true;
298  }
299
300
301
302  /**
303   * Indicates whether this tool should provide arguments for redirecting output
304   * to a file.  If this method returns {@code true}, then the tool will offer
305   * an "--outputFile" argument that will specify the path to a file to which
306   * all standard output and standard error content will be written, and it will
307   * also offer a "--teeToStandardOut" argument that can only be used if the
308   * "--outputFile" argument is present and will cause all output to be written
309   * to both the specified output file and to standard output.
310   *
311   * @return  {@code true} if this tool should provide arguments for redirecting
312   *          output to a file, or {@code false} if not.
313   */
314  @Override()
315  protected boolean supportsOutputFile()
316  {
317    return true;
318  }
319
320
321
322  /**
323   * Indicates whether this tool should default to interactively prompting for
324   * the bind password if a password is required but no argument was provided
325   * to indicate how to get the password.
326   *
327   * @return  {@code true} if this tool should default to interactively
328   *          prompting for the bind password, or {@code false} if not.
329   */
330  @Override()
331  protected boolean defaultToPromptForBindPassword()
332  {
333    return true;
334  }
335
336
337
338  /**
339   * Indicates whether this tool supports the use of a properties file for
340   * specifying default values for arguments that aren't specified on the
341   * command line.
342   *
343   * @return  {@code true} if this tool supports the use of a properties file
344   *          for specifying default values for arguments that aren't specified
345   *          on the command line, or {@code false} if not.
346   */
347  @Override()
348  public boolean supportsPropertiesFile()
349  {
350    return true;
351  }
352
353
354
355  /**
356   * Indicates whether this tool supports the ability to generate a debug log
357   * file.  If this method returns {@code true}, then the tool will expose
358   * additional arguments that can control debug logging.
359   *
360   * @return  {@code true} if this tool supports the ability to generate a debug
361   *          log file, or {@code false} if not.
362   */
363  @Override()
364  protected boolean supportsDebugLogging()
365  {
366    return true;
367  }
368
369
370
371  /**
372   * Indicates whether the LDAP-specific arguments should include alternate
373   * versions of all long identifiers that consist of multiple words so that
374   * they are available in both camelCase and dash-separated versions.
375   *
376   * @return  {@code true} if this tool should provide multiple versions of
377   *          long identifiers for LDAP-specific arguments, or {@code false} if
378   *          not.
379   */
380  @Override()
381  protected boolean includeAlternateLongIdentifiers()
382  {
383    return true;
384  }
385
386
387
388  /**
389   * Indicates whether this tool should provide a command-line argument that
390   * allows for low-level SSL debugging.  If this returns {@code true}, then an
391   * "--enableSSLDebugging}" argument will be added that sets the
392   * "javax.net.debug" system property to "all" before attempting any
393   * communication.
394   *
395   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
396   *          argument, or {@code false} if not.
397   */
398  @Override()
399  protected boolean supportsSSLDebugging()
400  {
401    return true;
402  }
403
404
405
406  /**
407   * Adds the arguments used by this program that aren't already provided by the
408   * generic {@code LDAPCommandLineTool} framework.
409   *
410   * @param  parser  The argument parser to which the arguments should be added.
411   *
412   * @throws  ArgumentException  If a problem occurs while adding the arguments.
413   */
414  @Override()
415  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
416         throws ArgumentException
417  {
418    // Save a reference to the argument parser.
419    this.parser = parser;
420
421    String description =
422         "Information about a control to include in the bind request.";
423    bindControls = new ControlArgument(null, "bindControl", false, 0, null,
424         description);
425    bindControls.addLongIdentifier("bind-control", true);
426    parser.addArgument(bindControls);
427
428
429    description = "Information about a control to include in compare requests.";
430    compareControls = new ControlArgument('J', "control", false, 0, null,
431         description);
432    parser.addArgument(compareControls);
433  }
434
435
436
437  /**
438   * {@inheritDoc}
439   */
440  @Override()
441  public void doExtendedNonLDAPArgumentValidation()
442         throws ArgumentException
443  {
444    // There must have been at least two trailing arguments provided.  The first
445    // must be in the form "attr:value".  All subsequent trailing arguments
446    // must be parsable as valid DNs.
447    final List<String> trailingArgs = parser.getTrailingArguments();
448    if (trailingArgs.size() < 2)
449    {
450      throw new ArgumentException("At least two trailing argument must be " +
451           "provided to specify the assertion criteria in the form " +
452           "'attr:value'.  All additional trailing arguments must be the " +
453           "DNs of the entries against which to perform the compare.");
454    }
455
456    final Iterator<String> argIterator = trailingArgs.iterator();
457    final String ava = argIterator.next();
458    if (ava.indexOf(':') < 1)
459    {
460      throw new ArgumentException("The first trailing argument value must " +
461           "specify the assertion criteria in the form 'attr:value'.");
462    }
463
464    while (argIterator.hasNext())
465    {
466      final String arg = argIterator.next();
467      try
468      {
469        new DN(arg);
470      }
471      catch (final Exception e)
472      {
473        Debug.debugException(e);
474        throw new ArgumentException(
475             "Unable to parse trailing argument '" + arg + "' as a valid DN.",
476             e);
477      }
478    }
479  }
480
481
482
483  /**
484   * {@inheritDoc}
485   */
486  @Override()
487  @NotNull()
488  protected List<Control> getBindControls()
489  {
490    return bindControls.getValues();
491  }
492
493
494
495  /**
496   * Performs the actual processing for this tool.  In this case, it gets a
497   * connection to the directory server and uses it to perform the requested
498   * comparisons.
499   *
500   * @return  The result code for the processing that was performed.
501   */
502  @Override()
503  @NotNull()
504  public ResultCode doToolProcessing()
505  {
506    // Make sure that at least two trailing arguments were provided, which will
507    // be the attribute value assertion and at least one entry DN.
508    final List<String> trailingArguments = parser.getTrailingArguments();
509    if (trailingArguments.isEmpty())
510    {
511      err("No attribute value assertion was provided.");
512      err();
513      err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
514      return ResultCode.PARAM_ERROR;
515    }
516    else if (trailingArguments.size() == 1)
517    {
518      err("No target entry DNs were provided.");
519      err();
520      err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
521      return ResultCode.PARAM_ERROR;
522    }
523
524
525    // Parse the attribute value assertion.
526    final String avaString = trailingArguments.get(0);
527    final int colonPos = avaString.indexOf(':');
528    if (colonPos <= 0)
529    {
530      err("Malformed attribute value assertion.");
531      err();
532      err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
533      return ResultCode.PARAM_ERROR;
534    }
535
536    final String attributeName = avaString.substring(0, colonPos);
537    final byte[] assertionValueBytes;
538    final int doubleColonPos = avaString.indexOf("::");
539    if (doubleColonPos == colonPos)
540    {
541      // There are two colons, so it's a base64-encoded assertion value.
542      try
543      {
544        assertionValueBytes = Base64.decode(avaString.substring(colonPos+2));
545      }
546      catch (final ParseException pe)
547      {
548        err("Unable to base64-decode the assertion value:  ",
549                    pe.getMessage());
550        err();
551        err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
552        return ResultCode.PARAM_ERROR;
553      }
554    }
555    else
556    {
557      // There is only a single colon, so it's a simple UTF-8 string.
558      assertionValueBytes =
559           StaticUtils.getBytes(avaString.substring(colonPos+1));
560    }
561
562
563    // Get the connection to the directory server.
564    final LDAPConnection connection;
565    try
566    {
567      connection = getConnection();
568      out("Connected to ", connection.getConnectedAddress(), ':',
569          connection.getConnectedPort());
570    }
571    catch (final LDAPException le)
572    {
573      err("Error connecting to the directory server:  ", le.getMessage());
574      return le.getResultCode();
575    }
576
577
578    // For each of the target entry DNs, process the compare.
579    ResultCode resultCode = ResultCode.SUCCESS;
580    CompareRequest compareRequest = null;
581    for (int i=1; i < trailingArguments.size(); i++)
582    {
583      final String targetDN = trailingArguments.get(i);
584      if (compareRequest == null)
585      {
586        compareRequest = new CompareRequest(targetDN, attributeName,
587                                            assertionValueBytes);
588        compareRequest.setControls(compareControls.getValues());
589      }
590      else
591      {
592        compareRequest.setDN(targetDN);
593      }
594
595      try
596      {
597        out("Processing compare request for entry ", targetDN);
598        final CompareResult result = connection.compare(compareRequest);
599        if (result.compareMatched())
600        {
601          out("The compare operation matched.");
602        }
603        else
604        {
605          out("The compare operation did not match.");
606        }
607      }
608      catch (final LDAPException le)
609      {
610        resultCode = le.getResultCode();
611        err("An error occurred while processing the request:  ",
612            le.getMessage());
613        err("Result Code:  ", le.getResultCode().intValue(), " (",
614            le.getResultCode().getName(), ')');
615        if (le.getMatchedDN() != null)
616        {
617          err("Matched DN:  ", le.getMatchedDN());
618        }
619        if (le.getReferralURLs() != null)
620        {
621          for (final String url : le.getReferralURLs())
622          {
623            err("Referral URL:  ", url);
624          }
625        }
626      }
627      out();
628    }
629
630
631    // Close the connection to the directory server and exit.
632    connection.close();
633    out();
634    out("Disconnected from the server");
635    return resultCode;
636  }
637
638
639
640  /**
641   * {@inheritDoc}
642   */
643  @Override()
644  @NotNull()
645  public LinkedHashMap<String[],String> getExampleUsages()
646  {
647    final LinkedHashMap<String[],String> examples =
648         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
649
650    final String[] args =
651    {
652      "--hostname", "server.example.com",
653      "--port", "389",
654      "--bindDN", "uid=admin,dc=example,dc=com",
655      "--bindPassword", "password",
656      "givenName:John",
657      "uid=jdoe,ou=People,dc=example,dc=com"
658    };
659    final String description =
660         "Attempt to determine whether the entry for user " +
661         "'uid=jdoe,ou=People,dc=example,dc=com' has a value of 'John' for " +
662         "the givenName attribute.";
663    examples.put(args, description);
664
665    return examples;
666  }
667}