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 *      Copyright 2006-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.quicksetup.ui;
028
029import org.opends.quicksetup.event.ButtonActionListener;
030import org.opends.quicksetup.event.ProgressUpdateListener;
031import org.opends.quicksetup.event.ButtonEvent;
032import org.opends.quicksetup.event.ProgressUpdateEvent;
033import org.opends.quicksetup.*;
034import org.opends.quicksetup.util.ProgressMessageFormatter;
035import org.opends.quicksetup.util.HtmlProgressMessageFormatter;
036import org.opends.quicksetup.util.BackgroundTask;
037import org.opends.server.util.SetupUtils;
038
039import static org.opends.quicksetup.util.Utils.*;
040import org.forgerock.i18n.LocalizableMessageBuilder;
041import org.forgerock.i18n.LocalizableMessage;
042import static org.opends.messages.QuickSetupMessages.*;
043import static com.forgerock.opendj.util.OperatingSystem.isMacOS;
044import static com.forgerock.opendj.cli.Utils.getThrowableMsg;
045
046import javax.swing.*;
047
048import java.awt.Cursor;
049import java.util.ArrayList;
050import java.util.List;
051import org.forgerock.i18n.slf4j.LocalizedLogger;
052
053import java.util.logging.Handler;
054import java.util.Map;
055
056/**
057 * This class is responsible for doing the following:
058 * <p>
059 * <ul>
060 * <li>Check whether we are installing or uninstalling.</li>
061 * <li>Performs all the checks and validation of the data provided by the user
062 * during the setup.</li>
063 * <li>It will launch also the installation once the user clicks on 'Finish' if
064 * we are installing the product.</li>
065 * </ul>
066 */
067public class QuickSetup implements ButtonActionListener, ProgressUpdateListener
068{
069
070  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
071
072  private GuiApplication application;
073
074  private CurrentInstallStatus installStatus;
075
076  private WizardStep currentStep;
077
078  private QuickSetupDialog dialog;
079
080  private LocalizableMessageBuilder progressDetails = new LocalizableMessageBuilder();
081
082  private ProgressDescriptor lastDescriptor;
083
084  private ProgressDescriptor lastDisplayedDescriptor;
085
086  private ProgressDescriptor descriptorToDisplay;
087
088  /** Update period of the dialogs. */
089  private static final int UPDATE_PERIOD = 500;
090
091  /** The full pathname of the MacOS X LaunchServices OPEN(1) helper. */
092  private static final String MAC_APPLICATIONS_OPENER = "/usr/bin/open";
093
094  /**
095   * This method creates the install/uninstall dialogs and to check the current
096   * install status. This method must be called outside the event thread because
097   * it can perform long operations which can make the user think that the UI is
098   * blocked.
099   *
100   * @param args
101   *          for the moment this parameter is not used but we keep it in order
102   *          to (in case of need) pass parameters through the command line.
103   */
104  public void initialize(String[] args)
105  {
106    ProgressMessageFormatter formatter = new HtmlProgressMessageFormatter();
107
108    installStatus = new CurrentInstallStatus();
109
110    application = Application.create();
111    application.setProgressMessageFormatter(formatter);
112    application.setCurrentInstallStatus(installStatus);
113    if (args != null)
114    {
115      application.setUserArguments(args);
116    }
117    else
118    {
119      application.setUserArguments(new String[] {});
120    }
121    try
122    {
123      initLookAndFeel();
124    }
125    catch (Throwable t)
126    {
127      // This is likely a bug.
128      t.printStackTrace();
129    }
130
131    /* In the calls to setCurrentStep the dialog will be created */
132    setCurrentStep(application.getFirstWizardStep());
133  }
134
135  /**
136   * This method displays the setup dialog.
137   * This method must be called from the event thread.
138   */
139  public void display()
140  {
141    getDialog().packAndShow();
142  }
143
144  /**
145   * ButtonActionListener implementation. It assumes that we are called in the
146   * event thread.
147   *
148   * @param ev
149   *          the ButtonEvent we receive.
150   */
151  public void buttonActionPerformed(ButtonEvent ev)
152  {
153    switch (ev.getButtonName())
154    {
155    case NEXT:
156      nextClicked();
157      break;
158    case CLOSE:
159      closeClicked();
160      break;
161    case FINISH:
162      finishClicked();
163      break;
164    case QUIT:
165      quitClicked();
166      break;
167    case CONTINUE_INSTALL:
168      continueInstallClicked();
169      break;
170    case PREVIOUS:
171      previousClicked();
172      break;
173    case LAUNCH_STATUS_PANEL:
174      launchStatusPanelClicked();
175      break;
176    case INPUT_PANEL_BUTTON:
177      inputPanelButtonClicked();
178      break;
179    default:
180      throw new IllegalArgumentException("Unknown button name: " + ev.getButtonName());
181    }
182  }
183
184  /**
185   * ProgressUpdateListener implementation. Here we take the ProgressUpdateEvent
186   * and create a ProgressDescriptor that will be used to update the progress
187   * dialog.
188   *
189   * @param ev
190   *          the ProgressUpdateEvent we receive.
191   * @see #runDisplayUpdater()
192   */
193  public void progressUpdate(ProgressUpdateEvent ev)
194  {
195    synchronized (this)
196    {
197      ProgressDescriptor desc = createProgressDescriptor(ev);
198      boolean isLastDescriptor = desc.getProgressStep().isLast();
199      if (isLastDescriptor)
200      {
201        lastDescriptor = desc;
202      }
203
204      descriptorToDisplay = desc;
205    }
206  }
207
208  /**
209   * This method is used to update the progress dialog.
210   * <p>
211   * We are receiving notifications from the installer and uninstaller (this
212   * class is a ProgressListener). However if we lots of notifications updating
213   * the progress panel every time we get a progress update can result of a lot
214   * of flickering. So the idea here is to have a minimal time between 2 updates
215   * of the progress dialog (specified by UPDATE_PERIOD).
216   *
217   * @see #progressUpdate(org.opends.quicksetup.event.ProgressUpdateEvent)
218   */
219  private void runDisplayUpdater()
220  {
221    boolean doPool = true;
222    while (doPool)
223    {
224      try
225      {
226        Thread.sleep(UPDATE_PERIOD);
227      }
228      catch (Exception ex) {}
229
230      synchronized (this)
231      {
232        final ProgressDescriptor desc = descriptorToDisplay;
233        if (desc != null)
234        {
235          if (desc != lastDisplayedDescriptor)
236          {
237            lastDisplayedDescriptor = desc;
238
239            SwingUtilities.invokeLater(new Runnable()
240            {
241              public void run()
242              {
243                if (application.isFinished() && !getCurrentStep().isFinishedStep())
244                {
245                  setCurrentStep(application.getFinishedStep());
246                }
247                getDialog().displayProgress(desc);
248              }
249            });
250          }
251          doPool = desc != lastDescriptor;
252        }
253      }
254    }
255  }
256
257  /** Method called when user clicks 'Next' button of the wizard. */
258  private void nextClicked()
259  {
260    final WizardStep cStep = getCurrentStep();
261    application.nextClicked(cStep, this);
262    BackgroundTask<?> worker = new NextClickedBackgroundTask(cStep);
263    getDialog().workerStarted();
264    worker.startBackgroundTask();
265  }
266
267  private void updateUserData(final WizardStep cStep)
268  {
269    BackgroundTask<?> worker = new BackgroundTask<Object>()
270    {
271      public Object processBackgroundTask() throws UserDataException
272      {
273        try
274        {
275          application.updateUserData(cStep, QuickSetup.this);
276        }
277        catch (UserDataException uide)
278        {
279          throw uide;
280        }
281        catch (Throwable t)
282        {
283          throw new UserDataException(cStep, getThrowableMsg(INFO_BUG_MSG.get(), t));
284        }
285        return null;
286      }
287
288      public void backgroundTaskCompleted(Object returnValue, Throwable throwable)
289      {
290        getDialog().workerFinished();
291
292        if (throwable != null)
293        {
294          UserDataException ude = (UserDataException) throwable;
295          if (ude instanceof UserDataConfirmationException)
296          {
297            if (displayConfirmation(ude.getMessageObject(), INFO_CONFIRMATION_TITLE.get()))
298            {
299              try
300              {
301                setCurrentStep(application.getNextWizardStep(cStep));
302              }
303              catch (Throwable t)
304              {
305                t.printStackTrace();
306              }
307            }
308          }
309          else
310          {
311            displayError(ude.getMessageObject(), INFO_ERROR_TITLE.get());
312          }
313        }
314        else
315        {
316          setCurrentStep(application.getNextWizardStep(cStep));
317        }
318        if (currentStep.isProgressStep())
319        {
320          launch();
321        }
322      }
323    };
324    getDialog().workerStarted();
325    worker.startBackgroundTask();
326  }
327
328  /** Method called when user clicks 'Finish' button of the wizard. */
329  private void finishClicked()
330  {
331    final WizardStep cStep = getCurrentStep();
332    if (application.finishClicked(cStep, this))
333    {
334      updateUserData(cStep);
335    }
336  }
337
338  /** Method called when user clicks 'Previous' button of the wizard. */
339  private void previousClicked()
340  {
341    WizardStep cStep = getCurrentStep();
342    application.previousClicked(cStep, this);
343    setCurrentStep(application.getPreviousWizardStep(cStep));
344  }
345
346  /** Method called when user clicks 'Quit' button of the wizard. */
347  private void quitClicked()
348  {
349    application.quitClicked(getCurrentStep(), this);
350  }
351
352  /**
353   * Method called when user clicks 'Continue' button in the case where there is
354   * something installed.
355   */
356  private void continueInstallClicked()
357  {
358    // TODO:  move this stuff to Installer?
359    application.forceToDisplay();
360    getDialog().forceToDisplay();
361    setCurrentStep(Step.WELCOME);
362  }
363
364  /** Method called when user clicks 'Close' button of the wizard. */
365  private void closeClicked()
366  {
367    application.closeClicked(getCurrentStep(), this);
368  }
369
370  private void launchStatusPanelClicked()
371  {
372    BackgroundTask<Object> worker = new BackgroundTask<Object>()
373    {
374      public Object processBackgroundTask() throws UserDataException
375      {
376        try
377        {
378          final Installation installation = Installation.getLocal();
379          final ProcessBuilder pb;
380
381          if (isMacOS())
382          {
383            List<String> cmd = new ArrayList<>();
384            cmd.add(MAC_APPLICATIONS_OPENER);
385            cmd.add(getScriptPath(getPath(installation.getControlPanelCommandFile())));
386            pb = new ProcessBuilder(cmd);
387          }
388          else
389          {
390            pb = new ProcessBuilder(getScriptPath(getPath(installation.getControlPanelCommandFile())));
391          }
392
393          Map<String, String> env = pb.environment();
394          env.put(SetupUtils.OPENDJ_JAVA_HOME, System.getProperty("java.home"));
395          final Process process = pb.start();
396          // Wait for 3 seconds. Assume that if the process has not exited everything went fine.
397          int returnValue = 0;
398          try
399          {
400            Thread.sleep(3000);
401          }
402          catch (Throwable t) {}
403
404          try
405          {
406            returnValue = process.exitValue();
407          }
408          catch (IllegalThreadStateException e)
409          {
410            // The process has not exited: assume that the status panel could be launched successfully.
411          }
412
413          if (returnValue != 0)
414          {
415            throw new Error(INFO_COULD_NOT_LAUNCH_CONTROL_PANEL_MSG.get().toString());
416          }
417        }
418        catch (Throwable t)
419        {
420          // This looks like a bug
421          t.printStackTrace();
422          throw new Error(INFO_COULD_NOT_LAUNCH_CONTROL_PANEL_MSG.get().toString());
423        }
424
425        return null;
426      }
427
428      public void backgroundTaskCompleted(Object returnValue, Throwable throwable)
429      {
430        getDialog().getFrame().setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
431        if (throwable != null)
432        {
433          displayError(LocalizableMessage.raw(throwable.getMessage()), INFO_ERROR_TITLE.get());
434        }
435      }
436    };
437    getDialog().getFrame().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
438    worker.startBackgroundTask();
439  }
440
441  /**
442   * This method tries to update the visibility of the steps panel. The contents
443   * are updated because the user clicked in one of the buttons that could make
444   * the steps panel to change.
445   */
446  private void inputPanelButtonClicked()
447  {
448    getDialog().getStepsPanel().updateStepVisibility(this);
449  }
450
451  /**
452   * Method called when we want to quit the setup (for instance when the user
453   * clicks on 'Close' or 'Quit' buttons and has confirmed that (s)he wants to
454   * quit the program.
455   */
456  public void quit()
457  {
458    logger.info(LocalizableMessage.raw("quitting application"));
459    flushLogs();
460    System.exit(0);
461  }
462
463  private void flushLogs()
464  {
465    java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(logger.getName());
466    Handler[] handlers = julLogger.getHandlers();
467    if (handlers != null)
468    {
469      for (Handler h : handlers)
470      {
471        h.flush();
472      }
473    }
474  }
475
476  /** Launch the QuickSetup application Open DS. */
477  public void launch()
478  {
479    application.addProgressUpdateListener(this);
480    new Thread(application, "Application Thread").start();
481    Thread t = new Thread(new Runnable()
482    {
483      public void run()
484      {
485        runDisplayUpdater();
486        WizardStep ws = application.getCurrentWizardStep();
487        getDialog().getButtonsPanel().updateButtons(ws);
488      }
489    });
490    t.start();
491  }
492
493  /**
494   * Get the current step.
495   *
496   * @return the currently displayed Step of the wizard.
497   */
498  private WizardStep getCurrentStep()
499  {
500    return currentStep;
501  }
502
503  /**
504   * Set the current step. This will basically make the required calls in the
505   * dialog to display the panel that corresponds to the step passed as
506   * argument.
507   *
508   * @param step
509   *          The step to be displayed.
510   */
511  public void setCurrentStep(WizardStep step)
512  {
513    if (step == null)
514    {
515      throw new NullPointerException("step is null");
516    }
517    currentStep = step;
518    application.setDisplayedWizardStep(step, application.getUserData(), getDialog());
519  }
520
521  /**
522   * Get the dialog that is displayed.
523   *
524   * @return the dialog.
525   */
526  public QuickSetupDialog getDialog()
527  {
528    if (dialog == null)
529    {
530      dialog = new QuickSetupDialog(application, installStatus, this);
531      dialog.addButtonActionListener(this);
532      application.setQuickSetupDialog(dialog);
533    }
534    return dialog;
535  }
536
537  /**
538   * Displays an error message dialog.
539   *
540   * @param msg
541   *          the error message.
542   * @param title
543   *          the title for the dialog.
544   */
545  public void displayError(LocalizableMessage msg, LocalizableMessage title)
546  {
547    if (isCli())
548    {
549      System.err.println(msg);
550    }
551    else
552    {
553      getDialog().displayError(msg, title);
554    }
555  }
556
557  /**
558   * Displays a confirmation message dialog.
559   *
560   * @param msg
561   *          the confirmation message.
562   * @param title
563   *          the title of the dialog.
564   * @return <CODE>true</CODE> if the user confirms the message, or
565   *         <CODE>false</CODE> if not.
566   */
567  public boolean displayConfirmation(LocalizableMessage msg, LocalizableMessage title)
568  {
569    return getDialog().displayConfirmation(msg, title);
570  }
571
572  /**
573   * Gets the string value for a given field name.
574   *
575   * @param fieldName
576   *          the field name object.
577   * @return the string value for the field name.
578   */
579  public String getFieldStringValue(FieldName fieldName)
580  {
581    final Object value = getFieldValue(fieldName);
582    if (value != null)
583    {
584      return String.valueOf(value);
585    }
586
587    return null;
588  }
589
590  /**
591   * Gets the value for a given field name.
592   *
593   * @param fieldName
594   *          the field name object.
595   * @return the value for the field name.
596   */
597  public Object getFieldValue(FieldName fieldName)
598  {
599    return getDialog().getFieldValue(fieldName);
600  }
601
602  /**
603   * Marks the fieldName as valid or invalid depending on the value of the
604   * invalid parameter. With the current implementation this implies basically
605   * using a red color in the label associated with the fieldName object. The
606   * color/style used to mark the label invalid is specified in UIFactory.
607   *
608   * @param fieldName
609   *          the field name object.
610   * @param invalid
611   *          whether to mark the field valid or invalid.
612   */
613  public void displayFieldInvalid(FieldName fieldName, boolean invalid)
614  {
615    getDialog().displayFieldInvalid(fieldName, invalid);
616  }
617
618  /** A method to initialize the look and feel. */
619  private void initLookAndFeel() throws Throwable
620  {
621    UIFactory.initialize();
622  }
623
624  /**
625   * A methods that creates an ProgressDescriptor based on the value of a
626   * ProgressUpdateEvent.
627   *
628   * @param ev
629   *          the ProgressUpdateEvent used to generate the ProgressDescriptor.
630   * @return the ProgressDescriptor.
631   */
632  private ProgressDescriptor createProgressDescriptor(ProgressUpdateEvent ev)
633  {
634    ProgressStep status = ev.getProgressStep();
635    LocalizableMessage newProgressLabel = ev.getCurrentPhaseSummary();
636    LocalizableMessage additionalDetails = ev.getNewLogs();
637    Integer ratio = ev.getProgressRatio();
638
639    if (additionalDetails != null)
640    {
641      progressDetails.append(additionalDetails);
642    }
643    /*
644     * Note: progressDetails might have a certain number of characters that
645     * break LocalizableMessage Formatter (for instance percentages).
646     * When fix for issue 2142 was committed it broke this code.
647     * So here we use LocalizableMessage.raw instead of calling directly progressDetails.toMessage
648     */
649    return new ProgressDescriptor(status, ratio, newProgressLabel, LocalizableMessage.raw(progressDetails.toString()));
650  }
651
652  /**
653   * This is a class used when the user clicks on next and that extends
654   * BackgroundTask.
655   */
656  private class NextClickedBackgroundTask extends BackgroundTask<Object>
657  {
658    private WizardStep cStep;
659
660    public NextClickedBackgroundTask(WizardStep cStep)
661    {
662      this.cStep = cStep;
663    }
664
665    public Object processBackgroundTask() throws UserDataException
666    {
667      try
668      {
669        application.updateUserData(cStep, QuickSetup.this);
670      }
671      catch (UserDataException uide)
672      {
673        throw uide;
674      }
675      catch (Throwable t)
676      {
677        throw new UserDataException(cStep, getThrowableMsg(INFO_BUG_MSG.get(), t));
678      }
679      return null;
680    }
681
682    public void backgroundTaskCompleted(Object returnValue, Throwable throwable)
683    {
684      getDialog().workerFinished();
685
686      if (throwable != null)
687      {
688        if (!(throwable instanceof UserDataException))
689        {
690          logger.warn(LocalizableMessage.raw("Unhandled exception.", throwable));
691        }
692        else
693        {
694          UserDataException ude = (UserDataException) throwable;
695          if (ude instanceof UserDataConfirmationException)
696          {
697            if (displayConfirmation(ude.getMessageObject(), INFO_CONFIRMATION_TITLE.get()))
698            {
699              setCurrentStep(application.getNextWizardStep(cStep));
700            }
701          }
702          else if (ude instanceof UserDataCertificateException)
703          {
704            final UserDataCertificateException ce = (UserDataCertificateException) ude;
705            CertificateDialog dlg = new CertificateDialog(getDialog().getFrame(), ce);
706            dlg.pack();
707            dlg.setVisible(true);
708            CertificateDialog.ReturnType answer = dlg.getUserAnswer();
709            if (answer != CertificateDialog.ReturnType.NOT_ACCEPTED)
710            {
711              // Retry the click but now with the certificate accepted.
712              final boolean acceptPermanently = answer == CertificateDialog.ReturnType.ACCEPTED_PERMANENTLY;
713              application.acceptCertificateForException(ce, acceptPermanently);
714              application.nextClicked(cStep, QuickSetup.this);
715              BackgroundTask<Object> worker = new NextClickedBackgroundTask(cStep);
716              getDialog().workerStarted();
717              worker.startBackgroundTask();
718            }
719          }
720          else
721          {
722            displayError(ude.getMessageObject(), INFO_ERROR_TITLE.get());
723          }
724        }
725      }
726      else
727      {
728        setCurrentStep(application.getNextWizardStep(cStep));
729      }
730    }
731  }
732}