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