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