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 the LDAP-specific arguments should include alternate
357   * versions of all long identifiers that consist of multiple words so that
358   * they are available in both camelCase and dash-separated versions.
359   *
360   * @return  {@code true} if this tool should provide multiple versions of
361   *          long identifiers for LDAP-specific arguments, or {@code false} if
362   *          not.
363   */
364  @Override()
365  protected boolean includeAlternateLongIdentifiers()
366  {
367    return true;
368  }
369
370
371
372  /**
373   * Indicates whether this tool should provide a command-line argument that
374   * allows for low-level SSL debugging.  If this returns {@code true}, then an
375   * "--enableSSLDebugging}" argument will be added that sets the
376   * "javax.net.debug" system property to "all" before attempting any
377   * communication.
378   *
379   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
380   *          argument, or {@code false} if not.
381   */
382  @Override()
383  protected boolean supportsSSLDebugging()
384  {
385    return true;
386  }
387
388
389
390  /**
391   * Adds the arguments used by this program that aren't already provided by the
392   * generic {@code LDAPCommandLineTool} framework.
393   *
394   * @param  parser  The argument parser to which the arguments should be added.
395   *
396   * @throws  ArgumentException  If a problem occurs while adding the arguments.
397   */
398  @Override()
399  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
400         throws ArgumentException
401  {
402    // Save a reference to the argument parser.
403    this.parser = parser;
404
405    String description =
406         "Information about a control to include in the bind request.";
407    bindControls = new ControlArgument(null, "bindControl", false, 0, null,
408         description);
409    bindControls.addLongIdentifier("bind-control", true);
410    parser.addArgument(bindControls);
411
412
413    description = "Information about a control to include in compare requests.";
414    compareControls = new ControlArgument('J', "control", false, 0, null,
415         description);
416    parser.addArgument(compareControls);
417  }
418
419
420
421  /**
422   * {@inheritDoc}
423   */
424  @Override()
425  public void doExtendedNonLDAPArgumentValidation()
426         throws ArgumentException
427  {
428    // There must have been at least two trailing arguments provided.  The first
429    // must be in the form "attr:value".  All subsequent trailing arguments
430    // must be parsable as valid DNs.
431    final List<String> trailingArgs = parser.getTrailingArguments();
432    if (trailingArgs.size() < 2)
433    {
434      throw new ArgumentException("At least two trailing argument must be " +
435           "provided to specify the assertion criteria in the form " +
436           "'attr:value'.  All additional trailing arguments must be the " +
437           "DNs of the entries against which to perform the compare.");
438    }
439
440    final Iterator<String> argIterator = trailingArgs.iterator();
441    final String ava = argIterator.next();
442    if (ava.indexOf(':') < 1)
443    {
444      throw new ArgumentException("The first trailing argument value must " +
445           "specify the assertion criteria in the form 'attr:value'.");
446    }
447
448    while (argIterator.hasNext())
449    {
450      final String arg = argIterator.next();
451      try
452      {
453        new DN(arg);
454      }
455      catch (final Exception e)
456      {
457        Debug.debugException(e);
458        throw new ArgumentException(
459             "Unable to parse trailing argument '" + arg + "' as a valid DN.",
460             e);
461      }
462    }
463  }
464
465
466
467  /**
468   * {@inheritDoc}
469   */
470  @Override()
471  @NotNull()
472  protected List<Control> getBindControls()
473  {
474    return bindControls.getValues();
475  }
476
477
478
479  /**
480   * Performs the actual processing for this tool.  In this case, it gets a
481   * connection to the directory server and uses it to perform the requested
482   * comparisons.
483   *
484   * @return  The result code for the processing that was performed.
485   */
486  @Override()
487  @NotNull()
488  public ResultCode doToolProcessing()
489  {
490    // Make sure that at least two trailing arguments were provided, which will
491    // be the attribute value assertion and at least one entry DN.
492    final List<String> trailingArguments = parser.getTrailingArguments();
493    if (trailingArguments.isEmpty())
494    {
495      err("No attribute value assertion was provided.");
496      err();
497      err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
498      return ResultCode.PARAM_ERROR;
499    }
500    else if (trailingArguments.size() == 1)
501    {
502      err("No target entry DNs were provided.");
503      err();
504      err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
505      return ResultCode.PARAM_ERROR;
506    }
507
508
509    // Parse the attribute value assertion.
510    final String avaString = trailingArguments.get(0);
511    final int colonPos = avaString.indexOf(':');
512    if (colonPos <= 0)
513    {
514      err("Malformed attribute value assertion.");
515      err();
516      err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
517      return ResultCode.PARAM_ERROR;
518    }
519
520    final String attributeName = avaString.substring(0, colonPos);
521    final byte[] assertionValueBytes;
522    final int doubleColonPos = avaString.indexOf("::");
523    if (doubleColonPos == colonPos)
524    {
525      // There are two colons, so it's a base64-encoded assertion value.
526      try
527      {
528        assertionValueBytes = Base64.decode(avaString.substring(colonPos+2));
529      }
530      catch (final ParseException pe)
531      {
532        err("Unable to base64-decode the assertion value:  ",
533                    pe.getMessage());
534        err();
535        err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
536        return ResultCode.PARAM_ERROR;
537      }
538    }
539    else
540    {
541      // There is only a single colon, so it's a simple UTF-8 string.
542      assertionValueBytes =
543           StaticUtils.getBytes(avaString.substring(colonPos+1));
544    }
545
546
547    // Get the connection to the directory server.
548    final LDAPConnection connection;
549    try
550    {
551      connection = getConnection();
552      out("Connected to ", connection.getConnectedAddress(), ':',
553          connection.getConnectedPort());
554    }
555    catch (final LDAPException le)
556    {
557      err("Error connecting to the directory server:  ", le.getMessage());
558      return le.getResultCode();
559    }
560
561
562    // For each of the target entry DNs, process the compare.
563    ResultCode resultCode = ResultCode.SUCCESS;
564    CompareRequest compareRequest = null;
565    for (int i=1; i < trailingArguments.size(); i++)
566    {
567      final String targetDN = trailingArguments.get(i);
568      if (compareRequest == null)
569      {
570        compareRequest = new CompareRequest(targetDN, attributeName,
571                                            assertionValueBytes);
572        compareRequest.setControls(compareControls.getValues());
573      }
574      else
575      {
576        compareRequest.setDN(targetDN);
577      }
578
579      try
580      {
581        out("Processing compare request for entry ", targetDN);
582        final CompareResult result = connection.compare(compareRequest);
583        if (result.compareMatched())
584        {
585          out("The compare operation matched.");
586        }
587        else
588        {
589          out("The compare operation did not match.");
590        }
591      }
592      catch (final LDAPException le)
593      {
594        resultCode = le.getResultCode();
595        err("An error occurred while processing the request:  ",
596            le.getMessage());
597        err("Result Code:  ", le.getResultCode().intValue(), " (",
598            le.getResultCode().getName(), ')');
599        if (le.getMatchedDN() != null)
600        {
601          err("Matched DN:  ", le.getMatchedDN());
602        }
603        if (le.getReferralURLs() != null)
604        {
605          for (final String url : le.getReferralURLs())
606          {
607            err("Referral URL:  ", url);
608          }
609        }
610      }
611      out();
612    }
613
614
615    // Close the connection to the directory server and exit.
616    connection.close();
617    out();
618    out("Disconnected from the server");
619    return resultCode;
620  }
621
622
623
624  /**
625   * {@inheritDoc}
626   */
627  @Override()
628  @NotNull()
629  public LinkedHashMap<String[],String> getExampleUsages()
630  {
631    final LinkedHashMap<String[],String> examples =
632         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
633
634    final String[] args =
635    {
636      "--hostname", "server.example.com",
637      "--port", "389",
638      "--bindDN", "uid=admin,dc=example,dc=com",
639      "--bindPassword", "password",
640      "givenName:John",
641      "uid=jdoe,ou=People,dc=example,dc=com"
642    };
643    final String description =
644         "Attempt to determine whether the entry for user " +
645         "'uid=jdoe,ou=People,dc=example,dc=com' has a value of 'John' for " +
646         "the givenName attribute.";
647    examples.put(args, description);
648
649    return examples;
650  }
651}