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