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   * Adds the arguments needed by this command-line tool to the provided
303   * argument parser which are not related to connecting or authenticating to
304   * the directory server.
305   *
306   * @param  parser  The argument parser to which the arguments should be added.
307   *
308   * @throws  ArgumentException  If a problem occurs while adding the arguments.
309   */
310  @Override()
311  public void addNonLDAPArguments(final ArgumentParser parser)
312         throws ArgumentException
313  {
314    baseDN = new DNArgument('b', "baseDN", true, 1, "{dn}",
315         "The base DN below which to dump the DNs of all entries in the " +
316              "Directory Server.");
317    baseDN.addLongIdentifier("base-dn", true);
318    parser.addArgument(baseDN);
319
320    outputFile = new FileArgument('f', "outputFile", false, 1, "{path}",
321         "The path of the output file to which the entry DNs will be " +
322              "written.  If this is not provided, then entry DNs will be " +
323              "written to standard output.", false, true, true, false);
324    outputFile.addLongIdentifier("output-file", true);
325    parser.addArgument(outputFile);
326  }
327
328
329
330  /**
331   * Retrieves the connection options that should be used for connections that
332   * are created with this command line tool.  Subclasses may override this
333   * method to use a custom set of connection options.
334   *
335   * @return  The connection options that should be used for connections that
336   *          are created with this command line tool.
337   */
338  @Override()
339  public LDAPConnectionOptions getConnectionOptions()
340  {
341    final LDAPConnectionOptions options = new LDAPConnectionOptions();
342
343    options.setUseSynchronousMode(true);
344    options.setResponseTimeoutMillis(0L);
345
346    return options;
347  }
348
349
350
351  /**
352   * Performs the core set of processing for this tool.
353   *
354   * @return  A result code that indicates whether the processing completed
355   *          successfully.
356   */
357  @Override()
358  public ResultCode doToolProcessing()
359  {
360    // Create the writer that will be used to write the DNs.
361    final File f = outputFile.getValue();
362    if (f == null)
363    {
364      outputStream = getOut();
365    }
366    else
367    {
368      try
369      {
370        outputStream =
371             new PrintStream(new BufferedOutputStream(new FileOutputStream(f)));
372      }
373      catch (final IOException ioe)
374      {
375        err("Unable to open output file '", f.getAbsolutePath(),
376             " for writing:  ", StaticUtils.getExceptionMessage(ioe));
377        return ResultCode.LOCAL_ERROR;
378      }
379    }
380
381
382    // Obtain a connection to the Directory Server.
383    final LDAPConnection conn;
384    try
385    {
386      conn = getConnection();
387    }
388    catch (final LDAPException le)
389    {
390      err("Unable to obtain a connection to the Directory Server:  ",
391          le.getExceptionMessage());
392      return le.getResultCode();
393    }
394
395
396    // Create the extended request.  Register this class as an intermediate
397    // response listener, and indicate that we don't want any response time
398    // limit.
399    final StreamDirectoryValuesExtendedRequest streamValuesRequest =
400         new StreamDirectoryValuesExtendedRequest(baseDN.getStringValue(),
401              SearchScope.SUB, false, null, 1000);
402    streamValuesRequest.setIntermediateResponseListener(this);
403    streamValuesRequest.setResponseTimeoutMillis(0L);
404
405
406    // Send the extended request to the server and get the result.
407    try
408    {
409      final ExtendedResult streamValuesResult =
410           conn.processExtendedOperation(streamValuesRequest);
411      err("Processing completed.  ", dnsWritten.get(), " DNs written.");
412      return streamValuesResult.getResultCode();
413    }
414    catch (final LDAPException le)
415    {
416      err("Unable  to send the stream directory values extended request to " +
417          "the Directory Server:  ", le.getExceptionMessage());
418      return le.getResultCode();
419    }
420    finally
421    {
422      if (f != null)
423      {
424        outputStream.close();
425      }
426
427      conn.close();
428    }
429  }
430
431
432
433  /**
434   * Retrieves a set of information that may be used to generate example usage
435   * information.  Each element in the returned map should consist of a map
436   * between an example set of arguments and a string that describes the
437   * behavior of the tool when invoked with that set of arguments.
438   *
439   * @return  A set of information that may be used to generate example usage
440   *          information.  It may be {@code null} or empty if no example usage
441   *          information is available.
442   */
443  @Override()
444  public LinkedHashMap<String[],String> getExampleUsages()
445  {
446    final LinkedHashMap<String[],String> exampleMap =
447         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
448
449    final String[] args =
450    {
451      "--hostname", "server.example.com",
452      "--port", "389",
453      "--bindDN", "uid=admin,dc=example,dc=com",
454      "--bindPassword", "password",
455      "--baseDN", "dc=example,dc=com",
456      "--outputFile", "example-dns.txt",
457    };
458    exampleMap.put(args,
459         "Dump all entry DNs at or below 'dc=example,dc=com' to the file " +
460              "'example-dns.txt'");
461
462    return exampleMap;
463  }
464
465
466
467  /**
468   * Indicates that the provided intermediate response has been returned by the
469   * server and may be processed by this intermediate response listener.  In
470   * this case, it will
471   *
472   * @param  intermediateResponse  The intermediate response that has been
473   *                               returned by the server.
474   */
475  @Override()
476  public void intermediateResponseReturned(
477                   final IntermediateResponse intermediateResponse)
478  {
479    // Try to parse the intermediate response as a stream directory values
480    // intermediate response.
481    final StreamDirectoryValuesIntermediateResponse streamValuesIR;
482    try
483    {
484      streamValuesIR =
485           new StreamDirectoryValuesIntermediateResponse(intermediateResponse);
486    }
487    catch (final LDAPException le)
488    {
489      err("Unable to parse an intermediate response message as a stream " +
490          "directory values intermediate response:  ",
491          le.getExceptionMessage());
492      return;
493    }
494
495    final String diagnosticMessage = streamValuesIR.getDiagnosticMessage();
496    if ((diagnosticMessage != null) && (! diagnosticMessage.isEmpty()))
497    {
498      err(diagnosticMessage);
499    }
500
501
502    final List<ASN1OctetString> values = streamValuesIR.getValues();
503    if ((values != null) && (! values.isEmpty()))
504    {
505      for (final ASN1OctetString s : values)
506      {
507        outputStream.println(s.toString());
508      }
509
510      final long updatedCount = dnsWritten.addAndGet(values.size());
511      if (outputFile.isPresent())
512      {
513        err(updatedCount, " DNs written.");
514      }
515    }
516  }
517}