001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Portions Copyright 2013-2016 ForgeRock AS.
015 */
016package org.opends.server.tools.upgrade;
017
018import static com.forgerock.opendj.cli.ArgumentConstants.*;
019import static com.forgerock.opendj.cli.Utils.*;
020import static com.forgerock.opendj.cli.CommonArguments.*;
021import static javax.security.auth.callback.TextOutputCallback.*;
022import static org.opends.messages.ToolMessages.*;
023import static org.opends.server.tools.upgrade.FormattedNotificationCallback.*;
024import static org.opends.server.tools.upgrade.Upgrade.*;
025
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.OutputStream;
029import java.io.PrintStream;
030import java.util.ArrayList;
031import java.util.List;
032
033import javax.security.auth.callback.Callback;
034import javax.security.auth.callback.CallbackHandler;
035import javax.security.auth.callback.ConfirmationCallback;
036import javax.security.auth.callback.TextOutputCallback;
037import javax.security.auth.callback.UnsupportedCallbackException;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.slf4j.LocalizedLogger;
041import org.opends.messages.RuntimeMessages;
042import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
043import org.opends.server.loggers.JDKLogging;
044import org.opends.server.util.StaticUtils;
045
046import com.forgerock.opendj.cli.ArgumentException;
047import com.forgerock.opendj.cli.BooleanArgument;
048import com.forgerock.opendj.cli.ClientException;
049import com.forgerock.opendj.cli.ConsoleApplication;
050import com.forgerock.opendj.cli.StringArgument;
051import com.forgerock.opendj.cli.SubCommandArgumentParser;
052
053/**
054 * This class provides the CLI used for upgrading the OpenDJ product.
055 */
056public final class UpgradeCli extends ConsoleApplication implements
057    CallbackHandler
058{
059  /** Upgrade's logger. */
060  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
061
062  /** The command-line argument parser. */
063  private final SubCommandArgumentParser parser;
064
065  /** The argument which should be used to specify the config file. */
066  private StringArgument configFile;
067
068  /** The argument which should be used to specify non interactive mode. */
069  private BooleanArgument noPrompt;
070  private BooleanArgument ignoreErrors;
071  private BooleanArgument force;
072  private BooleanArgument quietMode;
073  private BooleanArgument verbose;
074  private BooleanArgument acceptLicense;
075
076  /** The argument which should be used to request usage information. */
077  private BooleanArgument showUsageArgument;
078
079  /** Flag indicating whether the global arguments have already been initialized. */
080  private boolean globalArgumentsInitialized;
081
082  private UpgradeCli(InputStream in, OutputStream out, OutputStream err)
083  {
084    super(new PrintStream(out), new PrintStream(err));
085    this.parser =
086        new SubCommandArgumentParser(getClass().getName(),
087            INFO_UPGRADE_DESCRIPTION_CLI.get(), false);
088    this.parser.setVersionHandler(new DirectoryServerVersionHandler());
089    this.parser.setShortToolDescription(REF_SHORT_DESC_UPGRADE.get());
090    this.parser.setDocToolDescriptionSupplement(SUPPLEMENT_DESCRIPTION_UPGRADE_CLI.get());
091  }
092
093  /**
094   * Provides the command-line arguments to the main application for processing.
095   *
096   * @param args
097   *          The set of command-line arguments provided to this program.
098   */
099  public static void main(String[] args)
100  {
101    final int exitCode = main(args, true, System.out, System.err);
102    if (exitCode != 0)
103    {
104      System.exit(filterExitCode(exitCode));
105    }
106  }
107
108  /**
109   * Provides the command-line arguments to the main application for processing
110   * and returns the exit code as an integer.
111   *
112   * @param args
113   *          The set of command-line arguments provided to this program.
114   * @param initializeServer
115   *          Indicates whether to perform basic initialization (which should
116   *          not be done if the tool is running in the same JVM as the server).
117   * @param outStream
118   *          The output stream for standard output.
119   * @param errStream
120   *          The output stream for standard error.
121   * @return Zero to indicate that the program completed successfully, or
122   *         non-zero to indicate that an error occurred.
123   */
124  public static int main(String[] args, boolean initializeServer,
125      OutputStream outStream, OutputStream errStream)
126  {
127    final UpgradeCli app = new UpgradeCli(System.in, outStream, errStream);
128
129    // Run the application.
130    return app.run(args, initializeServer);
131  }
132
133  /** {@inheritDoc} */
134  @Override
135  public boolean isAdvancedMode()
136  {
137    return false;
138  }
139
140  /** {@inheritDoc} */
141  @Override
142  public boolean isInteractive()
143  {
144    return !noPrompt.isPresent();
145  }
146
147  /** {@inheritDoc} */
148  @Override
149  public boolean isMenuDrivenMode()
150  {
151    return false;
152  }
153
154  /** {@inheritDoc} */
155  @Override
156  public boolean isQuiet()
157  {
158    return quietMode.isPresent();
159  }
160
161  /** {@inheritDoc} */
162  @Override
163  public boolean isScriptFriendly()
164  {
165    return false;
166  }
167
168  /** {@inheritDoc} */
169  @Override
170  public boolean isVerbose()
171  {
172    return verbose.isPresent();
173  }
174
175  /**
176   * Force the upgrade. All critical questions will be forced to 'yes'.
177   *
178   * @return {@code true} if the upgrade process is forced.
179   */
180  private boolean isForceUpgrade()
181  {
182    return force.isPresent();
183  }
184
185  /**
186   * Force to ignore the errors during the upgrade process.
187   * Continues rather than fails.
188   *
189   * @return {@code true} if the errors are forced to be ignored.
190   */
191  private boolean isIgnoreErrors()
192  {
193    return ignoreErrors.isPresent();
194  }
195
196  /**
197   * Automatically accepts the license if it's present.
198   *
199   * @return {@code true} if license is accepted by default.
200   */
201  private boolean isAcceptLicense()
202  {
203    return acceptLicense.isPresent();
204  }
205
206  /** Initialize arguments provided by the command line. */
207  private void initializeGlobalArguments() throws ArgumentException
208  {
209    if (!globalArgumentsInitialized)
210    {
211      configFile = configFileArgument();
212      noPrompt = noPromptArgument();
213      verbose = verboseArgument();
214      quietMode = quietArgument();
215      ignoreErrors =
216              BooleanArgument.builder(OPTION_LONG_IGNORE_ERRORS)
217                      .description(INFO_UPGRADE_OPTION_IGNORE_ERRORS.get())
218                      .buildArgument();
219      force =
220              BooleanArgument.builder(OPTION_LONG_FORCE_UPGRADE)
221                      .description(INFO_UPGRADE_OPTION_FORCE.get(OPTION_LONG_NO_PROMPT))
222                      .buildArgument();
223      acceptLicense = acceptLicenseArgument();
224      showUsageArgument = showUsageArgument();
225
226
227      // Register the global arguments.
228      parser.addGlobalArgument(showUsageArgument);
229      parser.setUsageArgument(showUsageArgument, getOutputStream());
230      parser.addGlobalArgument(configFile);
231      parser.addGlobalArgument(noPrompt);
232      parser.addGlobalArgument(verbose);
233      parser.addGlobalArgument(quietMode);
234      parser.addGlobalArgument(force);
235      parser.addGlobalArgument(ignoreErrors);
236      parser.addGlobalArgument(acceptLicense);
237
238      globalArgumentsInitialized = true;
239    }
240  }
241
242  private int run(String[] args, boolean initializeServer)
243  {
244    // Initialize the arguments
245    try
246    {
247      initializeGlobalArguments();
248    }
249    catch (ArgumentException e)
250    {
251      final LocalizableMessage message = ERR_CANNOT_INITIALIZE_ARGS.get(e.getMessage());
252      getOutputStream().print(message);
253      return EXIT_CODE_ERROR;
254    }
255
256    // Parse the command-line arguments provided to this program.
257    try
258    {
259      parser.parseArguments(args);
260      if (isInteractive() && isQuiet())
261      {
262        final LocalizableMessage message =
263            ERR_UPGRADE_INCOMPATIBLE_ARGS.get(OPTION_LONG_QUIET,
264                "interactive mode");
265        getOutputStream().println(message);
266        return EXIT_CODE_ERROR;
267      }
268      if (isInteractive() && isForceUpgrade())
269      {
270        final LocalizableMessage message =
271            ERR_UPGRADE_INCOMPATIBLE_ARGS.get(OPTION_LONG_FORCE_UPGRADE,
272                "interactive mode");
273        getOutputStream().println(message);
274        return EXIT_CODE_ERROR;
275      }
276      if (isQuiet() && isVerbose())
277      {
278        final LocalizableMessage message =
279            ERR_UPGRADE_INCOMPATIBLE_ARGS.get(OPTION_LONG_QUIET,
280                OPTION_LONG_VERBOSE);
281        getOutputStream().println(message);
282        return EXIT_CODE_ERROR;
283      }
284    }
285    catch (ArgumentException ae)
286    {
287      parser.displayMessageAndUsageReference(getErrStream(), ERR_ERROR_PARSING_ARGS.get(ae.getMessage()));
288      return EXIT_CODE_ERROR;
289    }
290
291    // If the usage/version argument was provided, then we don't need
292    // to do anything else.
293    if (parser.usageOrVersionDisplayed())
294    {
295      return EXIT_CODE_SUCCESS;
296    }
297
298    // Main process
299    try
300    {
301      UpgradeLog.createLogFile();
302      JDKLogging.enableLoggingForOpenDJTool(UpgradeLog.getPrintStream());
303      logger.info(LocalizableMessage.raw("**** Upgrade of OpenDJ started ****"));
304      logger.info(RuntimeMessages.NOTE_INSTALL_DIRECTORY.get(UpgradeUtils.getInstallationPath()));
305      logger.info(RuntimeMessages.NOTE_INSTANCE_DIRECTORY.get(UpgradeUtils.getInstancePath()));
306
307      // Upgrade's context.
308      UpgradeContext context = new UpgradeContext(this)
309          .setIgnoreErrorsMode(isIgnoreErrors())
310          .setAcceptLicenseMode(isAcceptLicense())
311          .setInteractiveMode(isInteractive())
312          .setForceUpgradeMode(isForceUpgrade());
313
314      // Starts upgrade.
315      Upgrade.upgrade(context);
316    }
317    catch (ClientException ex)
318    {
319      return ex.getReturnCode();
320    }
321    catch (Exception ex)
322    {
323      println(Style.ERROR, ERR_UPGRADE_MAIN_UPGRADE_PROCESS.get(ex
324          .getMessage()), 0);
325
326      return EXIT_CODE_ERROR;
327    }
328    return Upgrade.isSuccess() ? EXIT_CODE_SUCCESS : EXIT_CODE_ERROR;
329  }
330
331  /** {@inheritDoc} */
332  @Override
333  public void handle(Callback[] callbacks) throws IOException,
334      UnsupportedCallbackException
335  {
336    for (final Callback c : callbacks)
337    {
338      // Displays progress eg. for a task.
339      if (c instanceof ProgressNotificationCallback)
340      {
341        final ProgressNotificationCallback pnc =
342            (ProgressNotificationCallback) c;
343        printProgressBar(pnc.getMessage(), pnc.getProgress(), 2);
344      }
345      else if (c instanceof FormattedNotificationCallback)
346      {
347        // Displays formatted notifications.
348        final FormattedNotificationCallback fnc =
349            (FormattedNotificationCallback) c;
350        switch (fnc.getMessageSubType())
351        {
352        case TITLE_CALLBACK:
353          println(Style.TITLE, LocalizableMessage.raw(fnc.getMessage()), 0);
354          logger.info(LocalizableMessage.raw(fnc.getMessage()));
355          break;
356        case SUBTITLE_CALLBACK:
357          println(Style.SUBTITLE, LocalizableMessage.raw(fnc.getMessage()),
358              4);
359          logger.info(LocalizableMessage.raw(fnc.getMessage()));
360          break;
361        case NOTICE_CALLBACK:
362          println(Style.NOTICE, LocalizableMessage.raw(fnc.getMessage()), 1);
363          logger.info(LocalizableMessage.raw(fnc.getMessage()));
364          break;
365        case ERROR_CALLBACK:
366          println(Style.ERROR, LocalizableMessage.raw(fnc.getMessage()), 1);
367          logger.error(LocalizableMessage.raw(fnc.getMessage()));
368          break;
369        case WARNING:
370          println(Style.WARNING, LocalizableMessage.raw(fnc.getMessage()), 2);
371          logger.warn(LocalizableMessage.raw(fnc.getMessage()));
372          break;
373        default:
374          logger.error(LocalizableMessage.raw("Unsupported message type: "
375            + fnc.getMessage()));
376          throw new IOException("Unsupported message type: ");
377        }
378      }
379      else if (c instanceof TextOutputCallback)
380      {
381        // Usual output text.
382        final TextOutputCallback toc = (TextOutputCallback) c;
383        if(toc.getMessageType() == TextOutputCallback.INFORMATION) {
384          logger.info(LocalizableMessage.raw(toc.getMessage()));
385          println(LocalizableMessage.raw(toc.getMessage()));
386        } else {
387          logger.error(LocalizableMessage.raw("Unsupported message type: "
388            + toc.getMessage()));
389          throw new IOException("Unsupported message type: ");
390        }
391      }
392      else if (c instanceof ConfirmationCallback)
393      {
394        final ConfirmationCallback cc = (ConfirmationCallback) c;
395        List<String> choices = new ArrayList<>();
396
397        final String defaultOption = getDefaultOption(cc.getDefaultOption());
398        StringBuilder prompt =
399            new StringBuilder(wrapText(cc.getPrompt(), MAX_LINE_WIDTH, 2));
400
401        // Default answers.
402        final List<String> yesNoDefaultResponses =
403            StaticUtils.arrayToList(
404              INFO_PROMPT_YES_COMPLETE_ANSWER.get().toString(),
405              INFO_PROMPT_YES_FIRST_LETTER_ANSWER.get().toString(),
406              INFO_PROMPT_NO_COMPLETE_ANSWER.get().toString(),
407              INFO_PROMPT_NO_FIRST_LETTER_ANSWER.get().toString());
408
409        // Generating prompt and possible answers list.
410        prompt.append(" ").append("(");
411        if (cc.getOptionType() == ConfirmationCallback.YES_NO_OPTION)
412        {
413          choices.addAll(yesNoDefaultResponses);
414          prompt.append(INFO_PROMPT_YES_COMPLETE_ANSWER.get())
415              .append("/")
416              .append(INFO_PROMPT_NO_COMPLETE_ANSWER.get());
417        }
418        else if (cc.getOptionType()
419            == ConfirmationCallback.YES_NO_CANCEL_OPTION)
420        {
421          choices.addAll(yesNoDefaultResponses);
422          choices.addAll(StaticUtils.arrayToList(
423              INFO_TASKINFO_CMD_CANCEL_CHAR.get().toString()));
424
425          prompt.append(" ")
426                .append("(").append(INFO_PROMPT_YES_COMPLETE_ANSWER.get())
427                .append("/").append(INFO_PROMPT_NO_COMPLETE_ANSWER.get())
428                .append("/").append(INFO_TASKINFO_CMD_CANCEL_CHAR.get());
429        }
430        prompt.append(")");
431
432        logger.info(LocalizableMessage.raw(cc.getPrompt()));
433
434        // Displays the output and
435        // while it hasn't a valid response, question is repeated.
436        if (isInteractive())
437        {
438          while (true)
439          {
440            String value = null;
441            try
442            {
443              value =
444                  readInput(LocalizableMessage.raw(prompt), defaultOption,
445                      Style.SUBTITLE);
446            }
447            catch (ClientException e)
448            {
449              logger.error(LocalizableMessage.raw(e.getMessage()));
450              break;
451            }
452
453            String valueLC = value.toLowerCase();
454            if ((valueLC.equals(INFO_PROMPT_YES_FIRST_LETTER_ANSWER.get().toString())
455                || valueLC.equals(INFO_PROMPT_YES_COMPLETE_ANSWER.get().toString()))
456                && choices.contains(value))
457            {
458              cc.setSelectedIndex(ConfirmationCallback.YES);
459              break;
460            }
461            else if ((valueLC.equals(INFO_PROMPT_NO_FIRST_LETTER_ANSWER.get().toString())
462                || valueLC.equals(INFO_PROMPT_NO_COMPLETE_ANSWER.get().toString()))
463                && choices.contains(value))
464            {
465              cc.setSelectedIndex(ConfirmationCallback.NO);
466              break;
467            }
468            else if (valueLC.equals(INFO_TASKINFO_CMD_CANCEL_CHAR.get().toString())
469                && choices.contains(value))
470            {
471              cc.setSelectedIndex(ConfirmationCallback.CANCEL);
472              break;
473            }
474            logger.info(LocalizableMessage.raw(value));
475          }
476        }
477        else // Non interactive mode :
478        {
479          // Force mode.
480          if (isForceUpgrade())
481          {
482            cc.setSelectedIndex(ConfirmationCallback.YES);
483          }
484          else // Default non interactive mode.
485          {
486            cc.setSelectedIndex(cc.getDefaultOption());
487          }
488          // Displays the prompt
489          prompt.append(" ").append(getDefaultOption(cc.getSelectedIndex()));
490          println(Style.SUBTITLE, LocalizableMessage.raw(prompt), 0);
491          logger.info(LocalizableMessage.raw(getDefaultOption(cc.getSelectedIndex())));
492        }
493      }
494      else
495      {
496        logger.error(LocalizableMessage.raw("Unrecognized Callback"));
497        throw new UnsupportedCallbackException(c, "Unrecognized Callback");
498      }
499    }
500  }
501
502
503
504  private static String getDefaultOption(final int defaultOption)
505  {
506    if (defaultOption == ConfirmationCallback.YES)
507    {
508      return INFO_PROMPT_YES_COMPLETE_ANSWER.get().toString();
509    }
510    else if (defaultOption == ConfirmationCallback.NO)
511    {
512      return INFO_PROMPT_NO_COMPLETE_ANSWER.get().toString();
513    }
514    else if (defaultOption == ConfirmationCallback.CANCEL)
515    {
516      return INFO_TASKINFO_CMD_CANCEL_CHAR.get().toString();
517    }
518    return null;
519  }
520}