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