001/*
002 * Copyright 2010-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2010-2024 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-2024 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      err("Processing completed.  ", dnsWritten.get(), " DNs written.");
470      return streamValuesResult.getResultCode();
471    }
472    catch (final LDAPException le)
473    {
474      err("Unable  to send the stream directory values extended request to " +
475          "the Directory Server:  ", le.getExceptionMessage());
476      return le.getResultCode();
477    }
478    finally
479    {
480      if (f != null)
481      {
482        outputStream.close();
483      }
484
485      conn.close();
486    }
487  }
488
489
490
491  /**
492   * Retrieves a set of information that may be used to generate example usage
493   * information.  Each element in the returned map should consist of a map
494   * between an example set of arguments and a string that describes the
495   * behavior of the tool when invoked with that set of arguments.
496   *
497   * @return  A set of information that may be used to generate example usage
498   *          information.  It may be {@code null} or empty if no example usage
499   *          information is available.
500   */
501  @Override()
502  @NotNull()
503  public LinkedHashMap<String[],String> getExampleUsages()
504  {
505    final LinkedHashMap<String[],String> exampleMap =
506         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
507
508    final String[] args =
509    {
510      "--hostname", "server.example.com",
511      "--port", "389",
512      "--bindDN", "uid=admin,dc=example,dc=com",
513      "--bindPassword", "password",
514      "--baseDN", "dc=example,dc=com",
515      "--outputFile", "example-dns.txt",
516    };
517    exampleMap.put(args,
518         "Dump all entry DNs at or below 'dc=example,dc=com' to the file " +
519              "'example-dns.txt'");
520
521    return exampleMap;
522  }
523
524
525
526  /**
527   * Indicates that the provided intermediate response has been returned by the
528   * server and may be processed by this intermediate response listener.  In
529   * this case, it will
530   *
531   * @param  intermediateResponse  The intermediate response that has been
532   *                               returned by the server.
533   */
534  @Override()
535  public void intermediateResponseReturned(
536                   @NotNull final IntermediateResponse intermediateResponse)
537  {
538    // Try to parse the intermediate response as a stream directory values
539    // intermediate response.
540    final StreamDirectoryValuesIntermediateResponse streamValuesIR;
541    try
542    {
543      streamValuesIR =
544           new StreamDirectoryValuesIntermediateResponse(intermediateResponse);
545    }
546    catch (final LDAPException le)
547    {
548      err("Unable to parse an intermediate response message as a stream " +
549          "directory values intermediate response:  ",
550          le.getExceptionMessage());
551      return;
552    }
553
554    final String diagnosticMessage = streamValuesIR.getDiagnosticMessage();
555    if ((diagnosticMessage != null) && (! diagnosticMessage.isEmpty()))
556    {
557      err(diagnosticMessage);
558    }
559
560
561    final List<ASN1OctetString> values = streamValuesIR.getValues();
562    if ((values != null) && (! values.isEmpty()))
563    {
564      for (final ASN1OctetString s : values)
565      {
566        outputStream.println(s.toString());
567      }
568
569      final long updatedCount = dnsWritten.addAndGet(values.size());
570      if (outputFile.isPresent())
571      {
572        err(updatedCount, " DNs written.");
573      }
574    }
575  }
576}