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