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