001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.util.cli;
028    
029    
030    
031    import static org.opends.messages.AdminToolMessages.*;
032    import static org.opends.messages.DSConfigMessages.*;
033    import static org.opends.messages.QuickSetupMessages.*;
034    import static org.opends.messages.UtilityMessages.*;
035    import static org.opends.server.util.ServerConstants.*;
036    import static org.opends.server.util.StaticUtils.*;
037    
038    import java.io.BufferedReader;
039    import java.io.EOFException;
040    import java.io.IOException;
041    import java.io.InputStream;
042    import java.io.InputStreamReader;
043    import java.io.OutputStream;
044    import java.io.PrintStream;
045    import java.io.Reader;
046    import java.util.logging.Level;
047    import java.util.logging.Logger;
048    
049    import javax.naming.NamingException;
050    import javax.naming.NoPermissionException;
051    import javax.naming.ldap.InitialLdapContext;
052    import javax.net.ssl.KeyManager;
053    import javax.net.ssl.TrustManager;
054    
055    import org.opends.admin.ads.util.ApplicationTrustManager;
056    import org.opends.admin.ads.util.ConnectionUtils;
057    import org.opends.admin.ads.util.OpendsCertificateException;
058    import org.opends.messages.Message;
059    import org.opends.quicksetup.util.Utils;
060    import org.opends.server.protocols.ldap.LDAPResultCode;
061    import org.opends.server.tools.ClientException;
062    import org.opends.server.types.NullOutputStream;
063    import org.opends.server.util.PasswordReader;
064    
065    
066    /**
067     * This class provides an abstract base class which can be used as the
068     * basis of a console-based application.
069     */
070    public abstract class ConsoleApplication {
071    
072      /**
073       * A null reader.
074       */
075      private static final class NullReader extends Reader {
076    
077        /**
078         * {@inheritDoc}
079         */
080        @Override
081        public void close() throws IOException {
082          // Do nothing.
083        }
084    
085    
086    
087        /**
088         * {@inheritDoc}
089         */
090        @Override
091        public int read(char[] cbuf, int off, int len) throws IOException {
092          return -1;
093        }
094      }
095    
096      // The error stream which this application should use.
097      private final PrintStream err;
098    
099      // The input stream reader which this application should use.
100      private final BufferedReader in;
101    
102      // The output stream which this application should use.
103      private final PrintStream out;
104    
105      /**
106       *  The maximum number of times we try to confirm.
107       */
108      protected final static int CONFIRMATION_MAX_TRIES = 5;
109    
110      /**
111       * Creates a new console application instance.
112       *
113       * @param in
114       *          The application input stream.
115       * @param out
116       *          The application output stream.
117       * @param err
118       *          The application error stream.
119       */
120      protected ConsoleApplication(BufferedReader in, PrintStream out,
121          PrintStream err) {
122        if (in != null) {
123          this.in = in;
124        } else {
125          this.in = new BufferedReader(new NullReader());
126        }
127    
128        if (out != null) {
129          this.out = out;
130        } else {
131          this.out = NullOutputStream.printStream();
132        }
133    
134        if (err != null) {
135          this.err = out;
136        } else {
137          this.err = NullOutputStream.printStream();
138        }
139      }
140    
141    
142    
143      /**
144       * Creates a new console application instance.
145       *
146       * @param in
147       *          The application input stream.
148       * @param out
149       *          The application output stream.
150       * @param err
151       *          The application error stream.
152       */
153      protected ConsoleApplication(InputStream in, OutputStream out,
154          OutputStream err) {
155        if (in != null) {
156          this.in = new BufferedReader(new InputStreamReader(in));
157        } else {
158          this.in = new BufferedReader(new NullReader());
159        }
160    
161        if (out != null) {
162          this.out = new PrintStream(out);
163        } else {
164          this.out = NullOutputStream.printStream();
165        }
166    
167        if (err != null) {
168          this.err = new PrintStream(err);
169        } else {
170          this.err = NullOutputStream.printStream();
171        }
172      }
173    
174    
175    
176      /**
177       * Interactively confirms whether a user wishes to perform an
178       * action. If the application is non-interactive, then the provided
179       * default is returned automatically.
180       *
181       * @param prompt
182       *          The prompt describing the action.
183       * @param defaultValue
184       *          The default value for the confirmation message. This
185       *          will be returned if the application is non-interactive
186       *          or if the user just presses return.
187       * @return Returns <code>true</code> if the user wishes the action
188       *         to be performed, or <code>false</code> if they refused,
189       *         or if an exception occurred.
190       * @throws CLIException
191       *           If the user's response could not be read from the
192       *           console for some reason.
193       */
194      public final boolean confirmAction(Message prompt, final boolean defaultValue)
195          throws CLIException {
196        if (!isInteractive()) {
197          return defaultValue;
198        }
199    
200        final Message yes = INFO_GENERAL_YES.get();
201        final Message no = INFO_GENERAL_NO.get();
202        final Message errMsg = ERR_CONSOLE_APP_CONFIRM.get(yes, no);
203        prompt = INFO_MENU_PROMPT_CONFIRM.get(prompt, yes, no, defaultValue ? yes
204            : no);
205    
206        ValidationCallback<Boolean> validator = new ValidationCallback<Boolean>() {
207    
208          public Boolean validate(ConsoleApplication app, String input) {
209            String ninput = input.toLowerCase().trim();
210            if (ninput.length() == 0) {
211              return defaultValue;
212            } else if (no.toString().startsWith(ninput)) {
213              return false;
214            } else if (yes.toString().startsWith(ninput)) {
215              return true;
216            } else {
217              // Try again...
218              app.println();
219              app.println(errMsg);
220              app.println();
221            }
222    
223            return null;
224          }
225        };
226    
227        return readValidatedInput(prompt, validator, CONFIRMATION_MAX_TRIES);
228      }
229    
230    
231    
232      /**
233       * Gets the application error stream.
234       *
235       * @return Returns the application error stream.
236       */
237      public final PrintStream getErrorStream() {
238        return err;
239      }
240    
241    
242    
243      /**
244       * Gets the application input stream.
245       *
246       * @return Returns the application input stream.
247       */
248      public final BufferedReader getInputStream() {
249        return in;
250      }
251    
252    
253    
254      /**
255       * Gets the application output stream.
256       *
257       * @return Returns the application output stream.
258       */
259      public final PrintStream getOutputStream() {
260        return out;
261      }
262    
263    
264    
265      /**
266       * Indicates whether or not the user has requested advanced mode.
267       *
268       * @return Returns <code>true</code> if the user has requested
269       *         advanced mode.
270       */
271      public abstract boolean isAdvancedMode();
272    
273    
274    
275      /**
276       * Indicates whether or not the user has requested interactive
277       * behavior.
278       *
279       * @return Returns <code>true</code> if the user has requested
280       *         interactive behavior.
281       */
282      public abstract boolean isInteractive();
283    
284    
285    
286      /**
287       * Indicates whether or not this console application is running in
288       * its menu-driven mode. This can be used to dictate whether output
289       * should go to the error stream or not. In addition, it may also
290       * dictate whether or not sub-menus should display a cancel option
291       * as well as a quit option.
292       *
293       * @return Returns <code>true</code> if this console application
294       *         is running in its menu-driven mode.
295       */
296      public abstract boolean isMenuDrivenMode();
297    
298    
299    
300      /**
301       * Indicates whether or not the user has requested quiet output.
302       *
303       * @return Returns <code>true</code> if the user has requested
304       *         quiet output.
305       */
306      public abstract boolean isQuiet();
307    
308    
309    
310      /**
311       * Indicates whether or not the user has requested script-friendly
312       * output.
313       *
314       * @return Returns <code>true</code> if the user has requested
315       *         script-friendly output.
316       */
317      public abstract boolean isScriptFriendly();
318    
319    
320    
321      /**
322       * Indicates whether or not the user has requested verbose output.
323       *
324       * @return Returns <code>true</code> if the user has requested
325       *         verbose output.
326       */
327      public abstract boolean isVerbose();
328    
329    
330    
331      /**
332       * Interactively prompts the user to press return to continue. This
333       * method should be called in situations where a user needs to be
334       * given a chance to read some documentation before continuing
335       * (continuing may cause the documentation to be scrolled out of
336       * view).
337       */
338      public final void pressReturnToContinue() {
339        Message msg = INFO_MENU_PROMPT_RETURN_TO_CONTINUE.get();
340        try {
341          readLineOfInput(msg);
342        } catch (CLIException e) {
343          // Ignore the exception - applications don't care.
344        }
345      }
346    
347    
348    
349      /**
350       * Displays a blank line to the error stream.
351       */
352      public final void println() {
353        err.println();
354      }
355    
356    
357    
358      /**
359       * Displays a message to the error stream.
360       *
361       * @param msg
362       *          The message.
363       */
364      public final void println(Message msg) {
365        err.println(wrapText(msg, MAX_LINE_WIDTH));
366      }
367    
368    
369      /**
370       * Displays a message to the error stream.
371       *
372       * @param msg
373       *          The message.
374       */
375      public final void print(Message msg) {
376        err.print(wrapText(msg, MAX_LINE_WIDTH));
377      }
378    
379      /**
380       * Displays a blank line to the output stream if we are not in quiet mode.
381       */
382      public final void printlnProgress() {
383        if (!isQuiet())
384        {
385          out.println();
386        }
387      }
388    
389    
390      /**
391       * Displays a message to the output stream if we are not in quiet mode.
392       *
393       * @param msg
394       *          The message.
395       */
396      public final void printProgress(Message msg) {
397        if (!isQuiet())
398        {
399          out.print(msg);
400        }
401      }
402    
403    
404      /**
405       * Displays a message to the error stream indented by the specified
406       * number of columns.
407       *
408       * @param msg
409       *          The message.
410       * @param indent
411       *          The number of columns to indent.
412       */
413      public final void println(Message msg, int indent) {
414        err.println(wrapText(msg, MAX_LINE_WIDTH, indent));
415      }
416    
417    
418    
419      /**
420       * Displays a message to the error stream if verbose mode is
421       * enabled.
422       *
423       * @param msg
424       *          The verbose message.
425       */
426      public final void printVerboseMessage(Message msg) {
427        if (isVerbose() || isInteractive()) {
428          err.println(wrapText(msg, MAX_LINE_WIDTH));
429        }
430      }
431    
432    
433    
434      /**
435       * Interactively retrieves a line of input from the console.
436       *
437       * @param prompt
438       *          The prompt.
439       * @return Returns the line of input, or <code>null</code> if the
440       *         end of input has been reached.
441       * @throws CLIException
442       *           If the line of input could not be retrieved for some
443       *           reason.
444       */
445      public final String readLineOfInput(Message prompt) throws CLIException {
446        if (prompt != null)
447        {
448          err.print(wrapText(prompt + " ", MAX_LINE_WIDTH));
449        }
450        try {
451          String s = in.readLine();
452          if (s == null) {
453            throw CLIException
454                .adaptInputException(new EOFException("End of input"));
455          } else {
456            return s;
457          }
458        } catch (IOException e) {
459          throw CLIException.adaptInputException(e);
460        }
461      }
462    
463    
464      /**
465       * Commodity method that interactively prompts (on error output) the user to
466       * provide a string value.  Any non-empty string will be allowed (the empty
467       * string will indicate that the default should be used, if there is one).
468       *
469       * @param  prompt        The prompt to present to the user.
470       * @param  defaultValue  The default value to assume if the user presses ENTER
471       *                       without typing anything, or <CODE>null</CODE> if
472       *                       there should not be a default and the user must
473       *                       explicitly provide a value.
474       *
475       * @throws CLIException
476       *           If the line of input could not be retrieved for some
477       *           reason.
478       * @return  The string value read from the user.
479       */
480      public String readInput(Message prompt, String defaultValue)
481      throws CLIException {
482        while (true) {
483          if (defaultValue != null) {
484            prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt.toString(),
485                defaultValue);
486          }
487          String response = readLineOfInput(prompt);
488    
489          if ("".equals(response)) {
490            if (defaultValue == null) {
491              print(INFO_ERROR_EMPTY_RESPONSE.get());
492            } else {
493              return defaultValue;
494            }
495          } else {
496            return response;
497          }
498        }
499      }
500    
501      /**
502       * Commodity method that interactively prompts (on error output) the user to
503       * provide a string value.  Any non-empty string will be allowed (the empty
504       * string will indicate that the default should be used, if there is one).
505       * If an error occurs a message will be logged to the provided logger.
506       *
507       * @param  prompt        The prompt to present to the user.
508       * @param  defaultValue  The default value to assume if the user presses ENTER
509       *                       without typing anything, or <CODE>null</CODE> if
510       *                       there should not be a default and the user must
511       *                       explicitly provide a value.
512       *
513       * @param logger the Logger to be used to log the error message.
514       * @return  The string value read from the user.
515       */
516      public String readInput(Message prompt, String defaultValue, Logger logger)
517      {
518        String s = defaultValue;
519        try
520        {
521          s = readInput(prompt, defaultValue);
522        }
523        catch (CLIException ce)
524        {
525          logger.log(Level.WARNING, "Error reading input: "+ce, ce);
526        }
527        return s;
528      }
529    
530      /**
531       * Interactively retrieves a password from the console.
532       *
533       * @param prompt
534       *          The password prompt.
535       * @return Returns the password.
536       * @throws CLIException
537       *           If the password could not be retrieved for some reason.
538       */
539      public final String readPassword(Message prompt) throws CLIException {
540        err.print(wrapText(prompt + " ", MAX_LINE_WIDTH));
541        char[] pwChars;
542        try {
543          pwChars = PasswordReader.readPassword();
544        } catch (Exception e) {
545          throw CLIException.adaptInputException(e);
546        }
547        return new String(pwChars);
548      }
549    
550      /**
551       * Commodity method that interactively retrieves a password from the
552       * console. If there is an error an error message is logged to the provided
553       * Logger and <CODE>null</CODE> is returned.
554       *
555       * @param prompt
556       *          The password prompt.
557       * @param logger the Logger to be used to log the error message.
558       * @return Returns the password.
559       */
560      protected final String readPassword(Message prompt, Logger logger)
561      {
562        String pwd = null;
563        try
564        {
565          pwd = readPassword(prompt);
566        }
567        catch (CLIException ce)
568        {
569          logger.log(Level.WARNING, "Error reading input: "+ce, ce);
570        }
571        return pwd;
572      }
573    
574      /**
575       * Interactively retrieves a port value from the console.
576       *
577       * @param prompt
578       *          The port prompt.
579       * @param defaultValue
580       *          The port default value.
581       * @return Returns the port.
582       * @throws CLIException
583       *           If the port could not be retrieved for some reason.
584       */
585      public final int readPort(Message prompt, final int defaultValue)
586      throws CLIException
587      {
588        ValidationCallback<Integer> callback = new ValidationCallback<Integer>()
589        {
590          public Integer validate(ConsoleApplication app, String input)
591              throws CLIException
592          {
593            String ninput = input.trim();
594            if (ninput.length() == 0)
595            {
596              return defaultValue;
597            }
598            else
599            {
600              try
601              {
602                int i = Integer.parseInt(ninput);
603                if (i < 1 || i > 65535)
604                {
605                  throw new NumberFormatException();
606                }
607                return i;
608              }
609              catch (NumberFormatException e)
610              {
611                // Try again...
612                app.println();
613                app.println(ERR_LDAP_CONN_BAD_PORT_NUMBER.get(ninput));
614                app.println();
615                return null;
616              }
617            }
618          }
619    
620        };
621    
622        if (defaultValue != -1) {
623          prompt = INFO_PROMPT_SINGLE_DEFAULT.get(prompt.toString(),
624              String.valueOf(defaultValue));
625        }
626    
627        return readValidatedInput(prompt, callback);
628      }
629    
630      /**
631       * Interactively prompts for user input and continues until valid
632       * input is provided.
633       *
634       * @param <T>
635       *          The type of decoded user input.
636       * @param prompt
637       *          The interactive prompt which should be displayed on each
638       *          input attempt.
639       * @param validator
640       *          An input validator responsible for validating and
641       *          decoding the user's response.
642       * @return Returns the decoded user's response.
643       * @throws CLIException
644       *           If an unexpected error occurred which prevented
645       *           validation.
646       */
647      public final <T> T readValidatedInput(Message prompt,
648          ValidationCallback<T> validator) throws CLIException {
649        while (true) {
650          String response = readLineOfInput(prompt);
651          T value = validator.validate(this, response);
652          if (value != null) {
653            return value;
654          }
655        }
656      }
657    
658      /**
659       * Interactively prompts for user input and continues until valid
660       * input is provided.
661       *
662       * @param <T>
663       *          The type of decoded user input.
664       * @param prompt
665       *          The interactive prompt which should be displayed on each
666       *          input attempt.
667       * @param validator
668       *          An input validator responsible for validating and
669       *          decoding the user's response.
670       * @param maxTries
671       *          The maximum number of tries that we can make.
672       * @return Returns the decoded user's response.
673       * @throws CLIException
674       *           If an unexpected error occurred which prevented
675       *           validation or if the maximum number of tries was reached.
676       */
677      public final <T> T readValidatedInput(Message prompt,
678          ValidationCallback<T> validator, int maxTries) throws CLIException {
679        int nTries = 0;
680        while (nTries < maxTries) {
681          String response = readLineOfInput(prompt);
682          T value = validator.validate(this, response);
683          if (value != null) {
684            return value;
685          }
686          nTries++;
687        }
688        throw new CLIException(ERR_TRIES_LIMIT_REACHED.get(maxTries));
689      }
690    
691      /**
692       * Commodity method that interactively confirms whether a user wishes to
693       * perform an action. If the application is non-interactive, then the provided
694       * default is returned automatically.  If there is an error an error message
695       * is logged to the provided Logger and the defaul value is returned.
696       *
697       * @param prompt
698       *          The prompt describing the action.
699       * @param defaultValue
700       *          The default value for the confirmation message. This
701       *          will be returned if the application is non-interactive
702       *          or if the user just presses return.
703       * @param logger the Logger to be used to log the error message.
704       * @return Returns <code>true</code> if the user wishes the action
705       *         to be performed, or <code>false</code> if they refused.
706       * @throws CLIException if the user did not provide valid answer after
707       *         a certain number of tries
708       *         (ConsoleApplication.CONFIRMATION_MAX_TRIES)
709       */
710      protected final boolean askConfirmation(Message prompt, boolean defaultValue,
711          Logger logger) throws CLIException
712      {
713        boolean v = defaultValue;
714    
715        boolean done = false;
716        int nTries = 0;
717    
718        while (!done && (nTries < CONFIRMATION_MAX_TRIES))
719        {
720          nTries++;
721          try
722          {
723            v = confirmAction(prompt, defaultValue);
724            done = true;
725          }
726          catch (CLIException ce)
727          {
728            if (ce.getMessageObject().getDescriptor().equals(
729                ERR_CONFIRMATION_TRIES_LIMIT_REACHED) ||
730                ce.getMessageObject().getDescriptor().equals(
731                    ERR_TRIES_LIMIT_REACHED))
732            {
733              throw ce;
734            }
735            logger.log(Level.WARNING, "Error reading input: "+ce, ce);
736    //      Try again...
737            println();
738          }
739        }
740    
741        if (!done)
742        {
743          // This means we reached the maximum number of tries
744          throw new CLIException(ERR_CONFIRMATION_TRIES_LIMIT_REACHED.get(
745              CONFIRMATION_MAX_TRIES));
746        }
747        return v;
748      }
749    
750      /**
751       * Returns an InitialLdapContext using the provided parameters.  We try
752       * to guarantee that the connection is able to read the configuration.
753       * @param host the host name.
754       * @param port the port to connect.
755       * @param useSSL whether to use SSL or not.
756       * @param useStartTLS whether to use StartTLS or not.
757       * @param bindDn the bind dn to be used.
758       * @param pwd the password.
759       * @param trustManager the trust manager.
760       * @return an InitialLdapContext connected.
761       * @throws NamingException if there was an error establishing the connection.
762       */
763      protected InitialLdapContext createAdministrativeContext(String host,
764          int port, boolean useSSL, boolean useStartTLS, String bindDn, String pwd,
765          ApplicationTrustManager trustManager)
766      throws NamingException
767      {
768        InitialLdapContext ctx;
769        String ldapUrl = ConnectionUtils.getLDAPUrl(host, port, useSSL);
770        if (useSSL)
771        {
772          ctx = Utils.createLdapsContext(ldapUrl, bindDn, pwd,
773              Utils.getDefaultLDAPTimeout(), null, trustManager);
774        }
775        else if (useStartTLS)
776        {
777          ctx = Utils.createStartTLSContext(ldapUrl, bindDn, pwd,
778              Utils.getDefaultLDAPTimeout(), null, trustManager,
779              null);
780        }
781        else
782        {
783          ctx = Utils.createLdapContext(ldapUrl, bindDn, pwd,
784              Utils.getDefaultLDAPTimeout(), null);
785        }
786        if (!ConnectionUtils.connectedAsAdministrativeUser(ctx))
787        {
788          throw new NoPermissionException(
789              ERR_NOT_ADMINISTRATIVE_USER.get().toString());
790        }
791        return ctx;
792      }
793    
794      /**
795       * Creates an Initial LDAP Context interacting with the user if the
796       * application is interactive.
797       * @param ci the LDAPConnectionConsoleInteraction object that is assumed
798       * to have been already run.
799       * @return the initial LDAP context or <CODE>null</CODE> if the user did
800       * not accept to trust the certificates.
801       * @throws ClientException if there was an error establishing the connection.
802       */
803      protected InitialLdapContext createInitialLdapContextInteracting(
804          LDAPConnectionConsoleInteraction ci) throws ClientException
805      {
806        // Interact with the user though the console to get
807        // LDAP connection information
808        String hostName = ConnectionUtils.getHostNameForLdapUrl(ci.getHostName());
809        Integer portNumber = ci.getPortNumber();
810        String bindDN = ci.getBindDN();
811        String bindPassword = ci.getBindPassword();
812        TrustManager trustManager = ci.getTrustManager();
813        KeyManager keyManager = ci.getKeyManager();
814    
815        InitialLdapContext ctx;
816    
817        if (ci.useSSL())
818        {
819          String ldapsUrl = "ldaps://" + hostName + ":" + portNumber;
820          while (true)
821          {
822            try
823            {
824              ctx = ConnectionUtils.createLdapsContext(ldapsUrl, bindDN,
825                  bindPassword, ConnectionUtils.getDefaultLDAPTimeout(), null,
826                  trustManager, keyManager);
827              ctx.reconnect(null);
828              break;
829            }
830            catch (NamingException e)
831            {
832              if ( isInteractive() && ci.isTrustStoreInMemory())
833              {
834                if ((e.getRootCause() != null)
835                    && (e.getRootCause().getCause()
836                        instanceof OpendsCertificateException))
837                {
838                  OpendsCertificateException oce =
839                    (OpendsCertificateException) e.getRootCause().getCause();
840                  String authType = null;
841                  if (trustManager instanceof ApplicationTrustManager)
842                  {
843                    ApplicationTrustManager appTrustManager =
844                      (ApplicationTrustManager)trustManager;
845                    authType = appTrustManager.getLastRefusedAuthType();
846                  }
847                    if (ci.checkServerCertificate(oce.getChain(), authType,
848                        hostName))
849                    {
850                      // If the certificate is trusted, update the trust manager.
851                      trustManager = ci.getTrustManager();
852    
853                      // Try to connect again.
854                      continue ;
855                    }
856                    else
857                    {
858                      // Assume user cancelled.
859                      return null;
860                    }
861                }
862                else
863                {
864                  Message message = ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(
865                      hostName, String.valueOf(portNumber));
866                  throw new ClientException(
867                      LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
868                }
869              }
870              Message message = ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(
871                  hostName, String.valueOf(portNumber));
872              throw new ClientException(
873                  LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
874            }
875          }
876        }
877        else if (ci.useStartTLS())
878        {
879          String ldapUrl = "ldap://" + hostName + ":" + portNumber;
880          while (true)
881          {
882            try
883            {
884              ctx = ConnectionUtils.createStartTLSContext(ldapUrl, bindDN,
885                  bindPassword, ConnectionUtils.getDefaultLDAPTimeout(), null,
886                  trustManager, keyManager, null);
887              ctx.reconnect(null);
888              break;
889            }
890            catch (NamingException e)
891            {
892              if ( isInteractive() && ci.isTrustStoreInMemory())
893              {
894                if ((e.getRootCause() != null)
895                    && (e.getRootCause().getCause()
896                        instanceof OpendsCertificateException))
897                {
898                  String authType = null;
899                  if (trustManager instanceof ApplicationTrustManager)
900                  {
901                    ApplicationTrustManager appTrustManager =
902                      (ApplicationTrustManager)trustManager;
903                    authType = appTrustManager.getLastRefusedAuthType();
904                  }
905                  OpendsCertificateException oce =
906                    (OpendsCertificateException) e.getRootCause().getCause();
907                    if (ci.checkServerCertificate(oce.getChain(), authType,
908                        hostName))
909                    {
910                      // If the certificate is trusted, update the trust manager.
911                      trustManager = ci.getTrustManager();
912    
913                      // Try to connect again.
914                      continue ;
915                    }
916                    else
917                    {
918                      // Assume user cancelled.
919                      return null;
920                    }
921                }
922                else
923                {
924                  Message message = ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(
925                      hostName, String.valueOf(portNumber));
926                  throw new ClientException(
927                      LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
928                }
929              }
930              Message message = ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(
931                  hostName, String.valueOf(portNumber));
932              throw new ClientException(
933                  LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
934            }
935          }
936        }
937        else
938        {
939          String ldapUrl = "ldap://" + hostName + ":" + portNumber;
940          while (true)
941          {
942            try
943            {
944              ctx = ConnectionUtils.createLdapContext(ldapUrl, bindDN,
945                  bindPassword, ConnectionUtils.getDefaultLDAPTimeout(), null);
946              ctx.reconnect(null);
947              break;
948            }
949            catch (NamingException e)
950            {
951              if ( isInteractive() && ci.isTrustStoreInMemory())
952              {
953                if ((e.getRootCause() != null)
954                    && (e.getRootCause().getCause()
955                        instanceof OpendsCertificateException))
956                {
957                  String authType = null;
958                  if (trustManager instanceof ApplicationTrustManager)
959                  {
960                    ApplicationTrustManager appTrustManager =
961                      (ApplicationTrustManager)trustManager;
962                    authType = appTrustManager.getLastRefusedAuthType();
963                  }
964                  OpendsCertificateException oce =
965                    (OpendsCertificateException) e.getRootCause().getCause();
966                    if (ci.checkServerCertificate(oce.getChain(), authType,
967                        hostName))
968                    {
969                      // If the certificate is trusted, update the trust manager.
970                      trustManager = ci.getTrustManager();
971    
972                      // Try to connect again.
973                      continue ;
974                    }
975                    else
976                    {
977                      // Assume user cancelled.
978                      return null;
979                    }
980                }
981                else
982                {
983                  Message message = ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(
984                      hostName, String.valueOf(portNumber));
985                  throw new ClientException(
986                      LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
987                }
988              }
989              Message message = ERR_DSCFG_ERROR_LDAP_FAILED_TO_CONNECT.get(
990                  hostName, String.valueOf(portNumber));
991              throw new ClientException(
992                  LDAPResultCode.CLIENT_SIDE_CONNECT_ERROR, message);
993            }
994          }
995        }
996        return ctx;
997      }
998    }