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