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