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 }