001    /*
002     * Copyright 2008-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 UnboundID Corp.
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     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.OutputStream;
026    import java.io.Serializable;
027    import java.text.ParseException;
028    import java.util.Iterator;
029    import java.util.LinkedHashMap;
030    import java.util.List;
031    
032    import com.unboundid.ldap.sdk.CompareRequest;
033    import com.unboundid.ldap.sdk.CompareResult;
034    import com.unboundid.ldap.sdk.Control;
035    import com.unboundid.ldap.sdk.DN;
036    import com.unboundid.ldap.sdk.LDAPConnection;
037    import com.unboundid.ldap.sdk.LDAPException;
038    import com.unboundid.ldap.sdk.ResultCode;
039    import com.unboundid.ldap.sdk.Version;
040    import com.unboundid.util.Base64;
041    import com.unboundid.util.Debug;
042    import com.unboundid.util.LDAPCommandLineTool;
043    import com.unboundid.util.StaticUtils;
044    import com.unboundid.util.ThreadSafety;
045    import com.unboundid.util.ThreadSafetyLevel;
046    import com.unboundid.util.args.ArgumentException;
047    import com.unboundid.util.args.ArgumentParser;
048    import 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)
076    public 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       * Adds the arguments used by this program that aren't already provided by the
351       * generic {@code LDAPCommandLineTool} framework.
352       *
353       * @param  parser  The argument parser to which the arguments should be added.
354       *
355       * @throws  ArgumentException  If a problem occurs while adding the arguments.
356       */
357      @Override()
358      public void addNonLDAPArguments(final ArgumentParser parser)
359             throws ArgumentException
360      {
361        // Save a reference to the argument parser.
362        this.parser = parser;
363    
364        String description =
365             "Information about a control to include in the bind request.";
366        bindControls = new ControlArgument(null, "bindControl", false, 0, null,
367             description);
368        bindControls.addLongIdentifier("bind-control");
369        parser.addArgument(bindControls);
370    
371    
372        description = "Information about a control to include in compare requests.";
373        compareControls = new ControlArgument('J', "control", false, 0, null,
374             description);
375        parser.addArgument(compareControls);
376      }
377    
378    
379    
380      /**
381       * {@inheritDoc}
382       */
383      @Override()
384      public void doExtendedNonLDAPArgumentValidation()
385             throws ArgumentException
386      {
387        // There must have been at least two trailing arguments provided.  The first
388        // must be in the form "attr:value".  All subsequent trailing arguments
389        // must be parsable as valid DNs.
390        final List<String> trailingArgs = parser.getTrailingArguments();
391        if (trailingArgs.size() < 2)
392        {
393          throw new ArgumentException("At least two trailing argument must be " +
394               "provided to specify the assertion criteria in the form " +
395               "'attr:value'.  All additional trailing arguments must be the " +
396               "DNs of the entries against which to perform the compare.");
397        }
398    
399        final Iterator<String> argIterator = trailingArgs.iterator();
400        final String ava = argIterator.next();
401        if (ava.indexOf(':') < 1)
402        {
403          throw new ArgumentException("The first trailing argument value must " +
404               "specify the assertion criteria in the form 'attr:value'.");
405        }
406    
407        while (argIterator.hasNext())
408        {
409          final String arg = argIterator.next();
410          try
411          {
412            new DN(arg);
413          }
414          catch (final Exception e)
415          {
416            Debug.debugException(e);
417            throw new ArgumentException(
418                 "Unable to parse trailing argument '" + arg + "' as a valid DN.",
419                 e);
420          }
421        }
422      }
423    
424    
425    
426      /**
427       * {@inheritDoc}
428       */
429      @Override()
430      protected List<Control> getBindControls()
431      {
432        return bindControls.getValues();
433      }
434    
435    
436    
437      /**
438       * Performs the actual processing for this tool.  In this case, it gets a
439       * connection to the directory server and uses it to perform the requested
440       * comparisons.
441       *
442       * @return  The result code for the processing that was performed.
443       */
444      @Override()
445      public ResultCode doToolProcessing()
446      {
447        // Make sure that at least two trailing arguments were provided, which will
448        // be the attribute value assertion and at least one entry DN.
449        final List<String> trailingArguments = parser.getTrailingArguments();
450        if (trailingArguments.isEmpty())
451        {
452          err("No attribute value assertion was provided.");
453          err();
454          err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
455          return ResultCode.PARAM_ERROR;
456        }
457        else if (trailingArguments.size() == 1)
458        {
459          err("No target entry DNs were provided.");
460          err();
461          err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
462          return ResultCode.PARAM_ERROR;
463        }
464    
465    
466        // Parse the attribute value assertion.
467        final String avaString = trailingArguments.get(0);
468        final int colonPos = avaString.indexOf(':');
469        if (colonPos <= 0)
470        {
471          err("Malformed attribute value assertion.");
472          err();
473          err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
474          return ResultCode.PARAM_ERROR;
475        }
476    
477        final String attributeName = avaString.substring(0, colonPos);
478        final byte[] assertionValueBytes;
479        final int doubleColonPos = avaString.indexOf("::");
480        if (doubleColonPos == colonPos)
481        {
482          // There are two colons, so it's a base64-encoded assertion value.
483          try
484          {
485            assertionValueBytes = Base64.decode(avaString.substring(colonPos+2));
486          }
487          catch (ParseException pe)
488          {
489            err("Unable to base64-decode the assertion value:  ",
490                        pe.getMessage());
491            err();
492            err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
493            return ResultCode.PARAM_ERROR;
494          }
495        }
496        else
497        {
498          // There is only a single colon, so it's a simple UTF-8 string.
499          assertionValueBytes =
500               StaticUtils.getBytes(avaString.substring(colonPos+1));
501        }
502    
503    
504        // Get the connection to the directory server.
505        final LDAPConnection connection;
506        try
507        {
508          connection = getConnection();
509          out("Connected to ", connection.getConnectedAddress(), ':',
510              connection.getConnectedPort());
511        }
512        catch (LDAPException le)
513        {
514          err("Error connecting to the directory server:  ", le.getMessage());
515          return le.getResultCode();
516        }
517    
518    
519        // For each of the target entry DNs, process the compare.
520        ResultCode resultCode = ResultCode.SUCCESS;
521        CompareRequest compareRequest = null;
522        for (int i=1; i < trailingArguments.size(); i++)
523        {
524          final String targetDN = trailingArguments.get(i);
525          if (compareRequest == null)
526          {
527            compareRequest = new CompareRequest(targetDN, attributeName,
528                                                assertionValueBytes);
529            compareRequest.setControls(compareControls.getValues());
530          }
531          else
532          {
533            compareRequest.setDN(targetDN);
534          }
535    
536          try
537          {
538            out("Processing compare request for entry ", targetDN);
539            final CompareResult result = connection.compare(compareRequest);
540            if (result.compareMatched())
541            {
542              out("The compare operation matched.");
543            }
544            else
545            {
546              out("The compare operation did not match.");
547            }
548          }
549          catch (LDAPException le)
550          {
551            resultCode = le.getResultCode();
552            err("An error occurred while processing the request:  ",
553                le.getMessage());
554            err("Result Code:  ", le.getResultCode().intValue(), " (",
555                le.getResultCode().getName(), ')');
556            if (le.getMatchedDN() != null)
557            {
558              err("Matched DN:  ", le.getMatchedDN());
559            }
560            if (le.getReferralURLs() != null)
561            {
562              for (final String url : le.getReferralURLs())
563              {
564                err("Referral URL:  ", url);
565              }
566            }
567          }
568          out();
569        }
570    
571    
572        // Close the connection to the directory server and exit.
573        connection.close();
574        out();
575        out("Disconnected from the server");
576        return resultCode;
577      }
578    
579    
580    
581      /**
582       * {@inheritDoc}
583       */
584      @Override()
585      public LinkedHashMap<String[],String> getExampleUsages()
586      {
587        final LinkedHashMap<String[],String> examples =
588             new LinkedHashMap<String[],String>();
589    
590        final String[] args =
591        {
592          "--hostname", "server.example.com",
593          "--port", "389",
594          "--bindDN", "uid=admin,dc=example,dc=com",
595          "--bindPassword", "password",
596          "givenName:John",
597          "uid=jdoe,ou=People,dc=example,dc=com"
598        };
599        final String description =
600             "Attempt to determine whether the entry for user " +
601             "'uid=jdoe,ou=People,dc=example,dc=com' has a value of 'John' for " +
602             "the givenName attribute.";
603        examples.put(args, description);
604    
605        return examples;
606      }
607    }