001/*
002 * Copyright 2010-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2015-2018 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 Alcatel-Lucent 8661
069 *   server products.  These classes provide support for proprietary
070 *   functionality or for external specifications that are not considered stable
071 *   or mature enough to be guaranteed to work in an interoperable way with
072 *   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  /**
174   * Retrieves the name of this tool.  It should be the name of the command used
175   * to invoke this tool.
176   *
177   * @return  The name for this tool.
178   */
179  @Override()
180  public String getToolName()
181  {
182    return "dump-dns";
183  }
184
185
186
187  /**
188   * Retrieves a human-readable description for this tool.
189   *
190   * @return  A human-readable description for this tool.
191   */
192  @Override()
193  public String getToolDescription()
194  {
195    return "Obtain a listing of all of the DNs for all entries below a " +
196         "specified base DN in the Directory Server.";
197  }
198
199
200
201  /**
202   * Retrieves the version string for this tool.
203   *
204   * @return  The version string for this tool.
205   */
206  @Override()
207  public String getToolVersion()
208  {
209    return Version.NUMERIC_VERSION_STRING;
210  }
211
212
213
214  /**
215   * Indicates whether this tool should provide support for an interactive mode,
216   * in which the tool offers a mode in which the arguments can be provided in
217   * a text-driven menu rather than requiring them to be given on the command
218   * line.  If interactive mode is supported, it may be invoked using the
219   * "--interactive" argument.  Alternately, if interactive mode is supported
220   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
221   * interactive mode may be invoked by simply launching the tool without any
222   * arguments.
223   *
224   * @return  {@code true} if this tool supports interactive mode, or
225   *          {@code false} if not.
226   */
227  @Override()
228  public boolean supportsInteractiveMode()
229  {
230    return true;
231  }
232
233
234
235  /**
236   * Indicates whether this tool defaults to launching in interactive mode if
237   * the tool is invoked without any command-line arguments.  This will only be
238   * used if {@link #supportsInteractiveMode()} returns {@code true}.
239   *
240   * @return  {@code true} if this tool defaults to using interactive mode if
241   *          launched without any command-line arguments, or {@code false} if
242   *          not.
243   */
244  @Override()
245  public boolean defaultsToInteractiveMode()
246  {
247    return true;
248  }
249
250
251
252  /**
253   * Indicates whether this tool should default to interactively prompting for
254   * the bind password if a password is required but no argument was provided
255   * to indicate how to get the password.
256   *
257   * @return  {@code true} if this tool should default to interactively
258   *          prompting for the bind password, or {@code false} if not.
259   */
260  @Override()
261  protected boolean defaultToPromptForBindPassword()
262  {
263    return true;
264  }
265
266
267
268  /**
269   * Indicates whether this tool supports the use of a properties file for
270   * specifying default values for arguments that aren't specified on the
271   * command line.
272   *
273   * @return  {@code true} if this tool supports the use of a properties file
274   *          for specifying default values for arguments that aren't specified
275   *          on the command line, or {@code false} if not.
276   */
277  @Override()
278  public boolean supportsPropertiesFile()
279  {
280    return true;
281  }
282
283
284
285  /**
286   * Indicates whether the LDAP-specific arguments should include alternate
287   * versions of all long identifiers that consist of multiple words so that
288   * they are available in both camelCase and dash-separated versions.
289   *
290   * @return  {@code true} if this tool should provide multiple versions of
291   *          long identifiers for LDAP-specific arguments, or {@code false} if
292   *          not.
293   */
294  @Override()
295  protected boolean includeAlternateLongIdentifiers()
296  {
297    return true;
298  }
299
300
301
302  /**
303   * Adds the arguments needed by this command-line tool to the provided
304   * argument parser which are not related to connecting or authenticating to
305   * the directory server.
306   *
307   * @param  parser  The argument parser to which the arguments should be added.
308   *
309   * @throws  ArgumentException  If a problem occurs while adding the arguments.
310   */
311  @Override()
312  public void addNonLDAPArguments(final ArgumentParser parser)
313         throws ArgumentException
314  {
315    baseDN = new DNArgument('b', "baseDN", true, 1, "{dn}",
316         "The base DN below which to dump the DNs of all entries in the " +
317              "Directory Server.");
318    baseDN.addLongIdentifier("base-dn", true);
319    parser.addArgument(baseDN);
320
321    outputFile = new FileArgument('f', "outputFile", false, 1, "{path}",
322         "The path of the output file to which the entry DNs will be " +
323              "written.  If this is not provided, then entry DNs will be " +
324              "written to standard output.", false, true, true, false);
325    outputFile.addLongIdentifier("output-file", true);
326    parser.addArgument(outputFile);
327  }
328
329
330
331  /**
332   * Retrieves the connection options that should be used for connections that
333   * are created with this command line tool.  Subclasses may override this
334   * method to use a custom set of connection options.
335   *
336   * @return  The connection options that should be used for connections that
337   *          are created with this command line tool.
338   */
339  @Override()
340  public LDAPConnectionOptions getConnectionOptions()
341  {
342    final LDAPConnectionOptions options = new LDAPConnectionOptions();
343
344    options.setUseSynchronousMode(true);
345    options.setResponseTimeoutMillis(0L);
346
347    return options;
348  }
349
350
351
352  /**
353   * Performs the core set of processing for this tool.
354   *
355   * @return  A result code that indicates whether the processing completed
356   *          successfully.
357   */
358  @Override()
359  public ResultCode doToolProcessing()
360  {
361    // Create the writer that will be used to write the DNs.
362    final File f = outputFile.getValue();
363    if (f == null)
364    {
365      outputStream = getOut();
366    }
367    else
368    {
369      try
370      {
371        outputStream =
372             new PrintStream(new BufferedOutputStream(new FileOutputStream(f)));
373      }
374      catch (final IOException ioe)
375      {
376        err("Unable to open output file '", f.getAbsolutePath(),
377             " for writing:  ", StaticUtils.getExceptionMessage(ioe));
378        return ResultCode.LOCAL_ERROR;
379      }
380    }
381
382
383    // Obtain a connection to the Directory Server.
384    final LDAPConnection conn;
385    try
386    {
387      conn = getConnection();
388    }
389    catch (final LDAPException le)
390    {
391      err("Unable to obtain a connection to the Directory Server:  ",
392          le.getExceptionMessage());
393      return le.getResultCode();
394    }
395
396
397    // Create the extended request.  Register this class as an intermediate
398    // response listener, and indicate that we don't want any response time
399    // limit.
400    final StreamDirectoryValuesExtendedRequest streamValuesRequest =
401         new StreamDirectoryValuesExtendedRequest(baseDN.getStringValue(),
402              SearchScope.SUB, false, null, 1000);
403    streamValuesRequest.setIntermediateResponseListener(this);
404    streamValuesRequest.setResponseTimeoutMillis(0L);
405
406
407    // Send the extended request to the server and get the result.
408    try
409    {
410      final ExtendedResult streamValuesResult =
411           conn.processExtendedOperation(streamValuesRequest);
412      err("Processing completed.  ", dnsWritten.get(), " DNs written.");
413      return streamValuesResult.getResultCode();
414    }
415    catch (final LDAPException le)
416    {
417      err("Unable  to send the stream directory values extended request to " +
418          "the Directory Server:  ", le.getExceptionMessage());
419      return le.getResultCode();
420    }
421    finally
422    {
423      if (f != null)
424      {
425        outputStream.close();
426      }
427
428      conn.close();
429    }
430  }
431
432
433
434  /**
435   * Retrieves a set of information that may be used to generate example usage
436   * information.  Each element in the returned map should consist of a map
437   * between an example set of arguments and a string that describes the
438   * behavior of the tool when invoked with that set of arguments.
439   *
440   * @return  A set of information that may be used to generate example usage
441   *          information.  It may be {@code null} or empty if no example usage
442   *          information is available.
443   */
444  @Override()
445  public LinkedHashMap<String[],String> getExampleUsages()
446  {
447    final LinkedHashMap<String[],String> exampleMap =
448         new LinkedHashMap<String[],String>(1);
449
450    final String[] args =
451    {
452      "--hostname", "server.example.com",
453      "--port", "389",
454      "--bindDN", "uid=admin,dc=example,dc=com",
455      "--bindPassword", "password",
456      "--baseDN", "dc=example,dc=com",
457      "--outputFile", "example-dns.txt",
458    };
459    exampleMap.put(args,
460         "Dump all entry DNs at or below 'dc=example,dc=com' to the file " +
461              "'example-dns.txt'");
462
463    return exampleMap;
464  }
465
466
467
468  /**
469   * Indicates that the provided intermediate response has been returned by the
470   * server and may be processed by this intermediate response listener.  In
471   * this case, it will
472   *
473   * @param  intermediateResponse  The intermediate response that has been
474   *                               returned by the server.
475   */
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.length() > 0))
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}