001/*
002 * Copyright 2010-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-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.unboundidds.examples;
022
023
024
025import java.io.BufferedOutputStream;
026import java.io.File;
027import java.io.FileOutputStream;
028import java.io.IOException;
029import java.io.OutputStream;
030import java.io.PrintStream;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.concurrent.atomic.AtomicLong;
034
035import com.unboundid.asn1.ASN1OctetString;
036import com.unboundid.ldap.sdk.ExtendedResult;
037import com.unboundid.ldap.sdk.LDAPConnection;
038import com.unboundid.ldap.sdk.LDAPConnectionOptions;
039import com.unboundid.ldap.sdk.LDAPException;
040import com.unboundid.ldap.sdk.IntermediateResponse;
041import com.unboundid.ldap.sdk.IntermediateResponseListener;
042import com.unboundid.ldap.sdk.ResultCode;
043import com.unboundid.ldap.sdk.SearchScope;
044import com.unboundid.ldap.sdk.Version;
045import com.unboundid.ldap.sdk.unboundidds.extensions.
046            StreamDirectoryValuesExtendedRequest;
047import com.unboundid.ldap.sdk.unboundidds.extensions.
048            StreamDirectoryValuesIntermediateResponse;
049import com.unboundid.util.LDAPCommandLineTool;
050import com.unboundid.util.StaticUtils;
051import com.unboundid.util.ThreadSafety;
052import com.unboundid.util.ThreadSafetyLevel;
053import com.unboundid.util.args.ArgumentException;
054import com.unboundid.util.args.ArgumentParser;
055import com.unboundid.util.args.DNArgument;
056import com.unboundid.util.args.FileArgument;
057
058
059
060/**
061 * This class provides a utility that uses the stream directory values extended
062 * operation in order to obtain a listing of all entry DNs below a specified
063 * base DN in the Directory Server.
064 * <BR>
065 * <BLOCKQUOTE>
066 *   <B>NOTE:</B>  This class, and other classes within the
067 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
068 *   supported for use against Ping Identity, UnboundID, and
069 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
070 *   for proprietary functionality or for external specifications that are not
071 *   considered stable or mature enough to be guaranteed to work in an
072 *   interoperable way with other types of LDAP servers.
073 * </BLOCKQUOTE>
074 * <BR>
075 * The APIs demonstrated by this example include:
076 * <UL>
077 *   <LI>The use of the stream directory values extended operation.</LI>
078 *   <LI>Intermediate response processing.</LI>
079 *   <LI>The LDAP command-line tool API.</LI>
080 *   <LI>Argument parsing.</LI>
081 * </UL>
082 */
083@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
084public final class DumpDNs
085       extends LDAPCommandLineTool
086       implements IntermediateResponseListener
087{
088  /**
089   * The serial version UID for this serializable class.
090   */
091  private static final long serialVersionUID = 774432759537092866L;
092
093
094
095  // The argument used to obtain the base DN.
096  private DNArgument baseDN;
097
098  // The argument used to obtain the output file.
099  private FileArgument outputFile;
100
101  // The number of DNs dumped.
102  private final AtomicLong dnsWritten;
103
104  // The print stream that will be used to output the DNs.
105  private PrintStream outputStream;
106
107
108
109  /**
110   * Parse the provided command line arguments and perform the appropriate
111   * processing.
112   *
113   * @param  args  The command line arguments provided to this program.
114   */
115  public static void main(final String[] args)
116  {
117    final ResultCode resultCode = main(args, System.out, System.err);
118    if (resultCode != ResultCode.SUCCESS)
119    {
120      System.exit(resultCode.intValue());
121    }
122  }
123
124
125
126  /**
127   * Parse the provided command line arguments and perform the appropriate
128   * processing.
129   *
130   * @param  args       The command line arguments provided to this program.
131   * @param  outStream  The output stream to which standard out should be
132   *                    written.  It may be {@code null} if output should be
133   *                    suppressed.
134   * @param  errStream  The output stream to which standard error should be
135   *                    written.  It may be {@code null} if error messages
136   *                    should be suppressed.
137   *
138   * @return  A result code indicating whether the processing was successful.
139   */
140  public static ResultCode main(final String[] args,
141                                final OutputStream outStream,
142                                final OutputStream errStream)
143  {
144    final DumpDNs tool = new DumpDNs(outStream, errStream);
145    return tool.runTool(args);
146  }
147
148
149
150  /**
151   * Creates a new instance of this tool.
152   *
153   * @param  outStream  The output stream to which standard out should be
154   *                    written.  It may be {@code null} if output should be
155   *                    suppressed.
156   * @param  errStream  The output stream to which standard error should be
157   *                    written.  It may be {@code null} if error messages
158   *                    should be suppressed.
159   */
160  public DumpDNs(final OutputStream outStream, final OutputStream errStream)
161  {
162    super(outStream, errStream);
163
164    baseDN       = null;
165    outputFile   = null;
166    outputStream = null;
167    dnsWritten   = new AtomicLong(0L);
168  }
169
170
171
172  /**
173   * Retrieves the name of this tool.  It should be the name of the command used
174   * to invoke this tool.
175   *
176   * @return  The name for this tool.
177   */
178  @Override()
179  public String getToolName()
180  {
181    return "dump-dns";
182  }
183
184
185
186  /**
187   * Retrieves a human-readable description for this tool.
188   *
189   * @return  A human-readable description for this tool.
190   */
191  @Override()
192  public String getToolDescription()
193  {
194    return "Obtain a listing of all of the DNs for all entries below a " +
195         "specified base DN in the Directory Server.";
196  }
197
198
199
200  /**
201   * Retrieves the version string for this tool.
202   *
203   * @return  The version string for this tool.
204   */
205  @Override()
206  public String getToolVersion()
207  {
208    return Version.NUMERIC_VERSION_STRING;
209  }
210
211
212
213  /**
214   * Indicates whether this tool should provide support for an interactive mode,
215   * in which the tool offers a mode in which the arguments can be provided in
216   * a text-driven menu rather than requiring them to be given on the command
217   * line.  If interactive mode is supported, it may be invoked using the
218   * "--interactive" argument.  Alternately, if interactive mode is supported
219   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
220   * interactive mode may be invoked by simply launching the tool without any
221   * arguments.
222   *
223   * @return  {@code true} if this tool supports interactive mode, or
224   *          {@code false} if not.
225   */
226  @Override()
227  public boolean supportsInteractiveMode()
228  {
229    return true;
230  }
231
232
233
234  /**
235   * Indicates whether this tool defaults to launching in interactive mode if
236   * the tool is invoked without any command-line arguments.  This will only be
237   * used if {@link #supportsInteractiveMode()} returns {@code true}.
238   *
239   * @return  {@code true} if this tool defaults to using interactive mode if
240   *          launched without any command-line arguments, or {@code false} if
241   *          not.
242   */
243  @Override()
244  public boolean defaultsToInteractiveMode()
245  {
246    return true;
247  }
248
249
250
251  /**
252   * Indicates whether this tool should default to interactively prompting for
253   * the bind password if a password is required but no argument was provided
254   * to indicate how to get the password.
255   *
256   * @return  {@code true} if this tool should default to interactively
257   *          prompting for the bind password, or {@code false} if not.
258   */
259  @Override()
260  protected boolean defaultToPromptForBindPassword()
261  {
262    return true;
263  }
264
265
266
267  /**
268   * Indicates whether this tool supports the use of a properties file for
269   * specifying default values for arguments that aren't specified on the
270   * command line.
271   *
272   * @return  {@code true} if this tool supports the use of a properties file
273   *          for specifying default values for arguments that aren't specified
274   *          on the command line, or {@code false} if not.
275   */
276  @Override()
277  public boolean supportsPropertiesFile()
278  {
279    return true;
280  }
281
282
283
284  /**
285   * Indicates whether the LDAP-specific arguments should include alternate
286   * versions of all long identifiers that consist of multiple words so that
287   * they are available in both camelCase and dash-separated versions.
288   *
289   * @return  {@code true} if this tool should provide multiple versions of
290   *          long identifiers for LDAP-specific arguments, or {@code false} if
291   *          not.
292   */
293  @Override()
294  protected boolean includeAlternateLongIdentifiers()
295  {
296    return true;
297  }
298
299
300
301  /**
302   * Indicates whether this tool should provide a command-line argument that
303   * allows for low-level SSL debugging.  If this returns {@code true}, then an
304   * "--enableSSLDebugging}" argument will be added that sets the
305   * "javax.net.debug" system property to "all" before attempting any
306   * communication.
307   *
308   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
309   *          argument, or {@code false} if not.
310   */
311  @Override()
312  protected boolean supportsSSLDebugging()
313  {
314    return true;
315  }
316
317
318
319  /**
320   * Adds the arguments needed by this command-line tool to the provided
321   * argument parser which are not related to connecting or authenticating to
322   * the directory server.
323   *
324   * @param  parser  The argument parser to which the arguments should be added.
325   *
326   * @throws  ArgumentException  If a problem occurs while adding the arguments.
327   */
328  @Override()
329  public void addNonLDAPArguments(final ArgumentParser parser)
330         throws ArgumentException
331  {
332    baseDN = new DNArgument('b', "baseDN", true, 1, "{dn}",
333         "The base DN below which to dump the DNs of all entries in the " +
334              "Directory Server.");
335    baseDN.addLongIdentifier("base-dn", true);
336    parser.addArgument(baseDN);
337
338    outputFile = new FileArgument('f', "outputFile", false, 1, "{path}",
339         "The path of the output file to which the entry DNs will be " +
340              "written.  If this is not provided, then entry DNs will be " +
341              "written to standard output.", false, true, true, false);
342    outputFile.addLongIdentifier("output-file", true);
343    parser.addArgument(outputFile);
344  }
345
346
347
348  /**
349   * Retrieves the connection options that should be used for connections that
350   * are created with this command line tool.  Subclasses may override this
351   * method to use a custom set of connection options.
352   *
353   * @return  The connection options that should be used for connections that
354   *          are created with this command line tool.
355   */
356  @Override()
357  public LDAPConnectionOptions getConnectionOptions()
358  {
359    final LDAPConnectionOptions options = new LDAPConnectionOptions();
360
361    options.setUseSynchronousMode(true);
362    options.setResponseTimeoutMillis(0L);
363
364    return options;
365  }
366
367
368
369  /**
370   * Performs the core set of processing for this tool.
371   *
372   * @return  A result code that indicates whether the processing completed
373   *          successfully.
374   */
375  @Override()
376  public ResultCode doToolProcessing()
377  {
378    // Create the writer that will be used to write the DNs.
379    final File f = outputFile.getValue();
380    if (f == null)
381    {
382      outputStream = getOut();
383    }
384    else
385    {
386      try
387      {
388        outputStream =
389             new PrintStream(new BufferedOutputStream(new FileOutputStream(f)));
390      }
391      catch (final IOException ioe)
392      {
393        err("Unable to open output file '", f.getAbsolutePath(),
394             " for writing:  ", StaticUtils.getExceptionMessage(ioe));
395        return ResultCode.LOCAL_ERROR;
396      }
397    }
398
399
400    // Obtain a connection to the Directory Server.
401    final LDAPConnection conn;
402    try
403    {
404      conn = getConnection();
405    }
406    catch (final LDAPException le)
407    {
408      err("Unable to obtain a connection to the Directory Server:  ",
409          le.getExceptionMessage());
410      return le.getResultCode();
411    }
412
413
414    // Create the extended request.  Register this class as an intermediate
415    // response listener, and indicate that we don't want any response time
416    // limit.
417    final StreamDirectoryValuesExtendedRequest streamValuesRequest =
418         new StreamDirectoryValuesExtendedRequest(baseDN.getStringValue(),
419              SearchScope.SUB, false, null, 1000);
420    streamValuesRequest.setIntermediateResponseListener(this);
421    streamValuesRequest.setResponseTimeoutMillis(0L);
422
423
424    // Send the extended request to the server and get the result.
425    try
426    {
427      final ExtendedResult streamValuesResult =
428           conn.processExtendedOperation(streamValuesRequest);
429      err("Processing completed.  ", dnsWritten.get(), " DNs written.");
430      return streamValuesResult.getResultCode();
431    }
432    catch (final LDAPException le)
433    {
434      err("Unable  to send the stream directory values extended request to " +
435          "the Directory Server:  ", le.getExceptionMessage());
436      return le.getResultCode();
437    }
438    finally
439    {
440      if (f != null)
441      {
442        outputStream.close();
443      }
444
445      conn.close();
446    }
447  }
448
449
450
451  /**
452   * Retrieves a set of information that may be used to generate example usage
453   * information.  Each element in the returned map should consist of a map
454   * between an example set of arguments and a string that describes the
455   * behavior of the tool when invoked with that set of arguments.
456   *
457   * @return  A set of information that may be used to generate example usage
458   *          information.  It may be {@code null} or empty if no example usage
459   *          information is available.
460   */
461  @Override()
462  public LinkedHashMap<String[],String> getExampleUsages()
463  {
464    final LinkedHashMap<String[],String> exampleMap =
465         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
466
467    final String[] args =
468    {
469      "--hostname", "server.example.com",
470      "--port", "389",
471      "--bindDN", "uid=admin,dc=example,dc=com",
472      "--bindPassword", "password",
473      "--baseDN", "dc=example,dc=com",
474      "--outputFile", "example-dns.txt",
475    };
476    exampleMap.put(args,
477         "Dump all entry DNs at or below 'dc=example,dc=com' to the file " +
478              "'example-dns.txt'");
479
480    return exampleMap;
481  }
482
483
484
485  /**
486   * Indicates that the provided intermediate response has been returned by the
487   * server and may be processed by this intermediate response listener.  In
488   * this case, it will
489   *
490   * @param  intermediateResponse  The intermediate response that has been
491   *                               returned by the server.
492   */
493  @Override()
494  public void intermediateResponseReturned(
495                   final IntermediateResponse intermediateResponse)
496  {
497    // Try to parse the intermediate response as a stream directory values
498    // intermediate response.
499    final StreamDirectoryValuesIntermediateResponse streamValuesIR;
500    try
501    {
502      streamValuesIR =
503           new StreamDirectoryValuesIntermediateResponse(intermediateResponse);
504    }
505    catch (final LDAPException le)
506    {
507      err("Unable to parse an intermediate response message as a stream " +
508          "directory values intermediate response:  ",
509          le.getExceptionMessage());
510      return;
511    }
512
513    final String diagnosticMessage = streamValuesIR.getDiagnosticMessage();
514    if ((diagnosticMessage != null) && (! diagnosticMessage.isEmpty()))
515    {
516      err(diagnosticMessage);
517    }
518
519
520    final List<ASN1OctetString> values = streamValuesIR.getValues();
521    if ((values != null) && (! values.isEmpty()))
522    {
523      for (final ASN1OctetString s : values)
524      {
525        outputStream.println(s.toString());
526      }
527
528      final long updatedCount = dnsWritten.addAndGet(values.size());
529      if (outputFile.isPresent())
530      {
531        err(updatedCount, " DNs written.");
532      }
533    }
534  }
535}