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