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