/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2010 Sun Microsystems, Inc.
* Portions Copyright 2011 ForgeRock AS
*/
package org.opends.quicksetup.ui;
import org.opends.quicksetup.event.ButtonActionListener;
import org.opends.quicksetup.event.ProgressUpdateListener;
import org.opends.quicksetup.event.ButtonEvent;
import org.opends.quicksetup.event.ProgressUpdateEvent;
import org.opends.quicksetup.*;
import org.opends.quicksetup.util.ProgressMessageFormatter;
import org.opends.quicksetup.util.HtmlProgressMessageFormatter;
import org.opends.quicksetup.util.BackgroundTask;
import org.opends.server.util.SetupUtils;
import static org.opends.quicksetup.util.Utils.*;
import org.opends.messages.MessageBuilder;
import org.opends.messages.Message;
import static org.opends.messages.QuickSetupMessages.*;
import javax.swing.*;
import java.awt.Cursor;
import java.util.ArrayList;
import java.util.logging.Logger;
import java.util.logging.Level;
import java.util.logging.Handler;
import java.util.Map;
/**
* This class is responsible for doing the following:
*
* Check whether we are installing or uninstalling and which type of
* installation we are running.
*
* Performs all the checks and validation of the data provided by the user
* during the setup.
*
* It will launch also the installation once the user clicks on 'Finish' if we
* are installing the product.
*
* If we are running a web start installation it will start the background
* downloading of the jar files that are required to perform the installation
* (OpenDS.jar, je.jar, etc.). The global idea is to force the user to
* download just one jar file (quicksetup.jar) to launch the Web Start
* installer. Until this class is not finished the WebStart Installer will be
* on the ProgressStep.DOWNLOADING step.
*
*/
public class QuickSetup implements ButtonActionListener, ProgressUpdateListener
{
static private final Logger LOG =
Logger.getLogger(QuickSetup.class.getName());
private GuiApplication application;
private CurrentInstallStatus installStatus;
private WizardStep currentStep;
private QuickSetupDialog dialog;
private MessageBuilder progressDetails = new MessageBuilder();
private ProgressDescriptor lastDescriptor;
private ProgressDescriptor lastDisplayedDescriptor;
private ProgressDescriptor descriptorToDisplay;
// Update period of the dialogs.
private static final int UPDATE_PERIOD = 500;
// The full pathname of the MacOS X LaunchServices OPEN(1) helper.
private static final String MAC_APPLICATIONS_OPENER = "/usr/bin/open";
/**
* This method creates the install/uninstall dialogs and to check the current
* install status. This method must be called outside the event thread because
* it can perform long operations which can make the user think that the UI is
* blocked.
*
* @param args for the moment this parameter is not used but we keep it in
* order to (in case of need) pass parameters through the command line.
*/
public void initialize(String[] args)
{
ProgressMessageFormatter formatter = new HtmlProgressMessageFormatter();
installStatus = new CurrentInstallStatus();
application = Application.create();
application.setProgressMessageFormatter(formatter);
application.setCurrentInstallStatus(installStatus);
if (args != null)
{
application.setUserArguments(args);
}
else
{
application.setUserArguments(new String[]{});
}
try
{
initLookAndFeel();
}
catch (Throwable t)
{
// This is likely a bug.
t.printStackTrace();
}
/* In the calls to setCurrentStep the dialog will be created */
setCurrentStep(application.getFirstWizardStep());
}
/**
* This method displays the setup dialog. This method must be called from the
* event thread.
*/
public void display()
{
getDialog().packAndShow();
}
/**
* ButtonActionListener implementation. It assumes that we are called in the
* event thread.
*
* @param ev the ButtonEvent we receive.
*/
public void buttonActionPerformed(ButtonEvent ev)
{
switch (ev.getButtonName())
{
case NEXT:
nextClicked();
break;
case CLOSE:
closeClicked();
break;
case FINISH:
finishClicked();
break;
case QUIT:
quitClicked();
break;
case CONTINUE_INSTALL:
continueInstallClicked();
break;
case PREVIOUS:
previousClicked();
break;
case LAUNCH_STATUS_PANEL:
launchStatusPanelClicked();
break;
case INPUT_PANEL_BUTTON:
inputPanelButtonClicked();
break;
default:
throw new IllegalArgumentException("Unknown button name: "
+ ev.getButtonName());
}
}
/**
* ProgressUpdateListener implementation. Here we take the
* ProgressUpdateEvent and create a ProgressDescriptor that
* will be used to update the progress dialog.
*
* @param ev the ProgressUpdateEvent we receive.
*
* @see #runDisplayUpdater()
*/
public void progressUpdate(ProgressUpdateEvent ev)
{
synchronized (this)
{
ProgressDescriptor desc = createProgressDescriptor(ev);
boolean isLastDescriptor = desc.getProgressStep().isLast();
if (isLastDescriptor)
{
lastDescriptor = desc;
}
descriptorToDisplay = desc;
}
}
/**
* This method is used to update the progress dialog.
*
* We are receiving notifications from the installer and uninstaller (this
* class is a ProgressListener). However if we lots of notifications updating
* the progress panel every time we get a progress update can result of a lot
* of flickering. So the idea here is to have a minimal time between 2 updates
* of the progress dialog (specified by UPDATE_PERIOD).
*
* @see #progressUpdate(org.opends.quicksetup.event.ProgressUpdateEvent)
*/
private void runDisplayUpdater()
{
boolean doPool = true;
while (doPool)
{
try
{
Thread.sleep(UPDATE_PERIOD);
} catch (Exception ex)
{
}
synchronized (this) {
final ProgressDescriptor desc = descriptorToDisplay;
if (desc != null) {
if (desc != lastDisplayedDescriptor) {
lastDisplayedDescriptor = desc;
SwingUtilities.invokeLater(new Runnable() {
public void run() {
if (application.isFinished() &&
!getCurrentStep().isFinishedStep())
{
setCurrentStep(application.getFinishedStep());
}
getDialog().displayProgress(desc);
}
});
}
doPool = desc != lastDescriptor;
}
}
}
}
/**
* Method called when user clicks 'Next' button of the wizard.
*
*/
private void nextClicked()
{
final WizardStep cStep = getCurrentStep();
application.nextClicked(cStep, this);
BackgroundTask<?> worker = new NextClickedBackgroundTask(cStep);
getDialog().workerStarted();
worker.startBackgroundTask();
}
private void updateUserData(final WizardStep cStep) {
BackgroundTask<?> worker = new BackgroundTask<Object>() {
public Object processBackgroundTask() throws UserDataException {
try {
application.updateUserData(cStep, QuickSetup.this);
}
catch (UserDataException uide) {
throw uide;
}
catch (Throwable t) {
throw new UserDataException(cStep,
getThrowableMsg(INFO_BUG_MSG.get(), t));
}
return null;
}
public void backgroundTaskCompleted(Object returnValue,
Throwable throwable) {
getDialog().workerFinished();
if (throwable != null) {
UserDataException ude = (UserDataException) throwable;
if (ude instanceof UserDataConfirmationException)
{
if (displayConfirmation(ude.getMessageObject(),
INFO_CONFIRMATION_TITLE.get()))
{
try
{
setCurrentStep(application.getNextWizardStep(cStep));
}
catch (Throwable t)
{
t.printStackTrace();
}
}
}
else
{
displayError(ude.getMessageObject(), INFO_ERROR_TITLE.get());
}
} else {
setCurrentStep(application.getNextWizardStep(cStep));
}
if (currentStep.isProgressStep()) {
launch();
}
}
};
getDialog().workerStarted();
worker.startBackgroundTask();
}
/**
* Method called when user clicks 'Finish' button of the wizard.
*
*/
private void finishClicked()
{
final WizardStep cStep = getCurrentStep();
if (application.finishClicked(cStep, this)) {
updateUserData(cStep);
}
}
/**
* Method called when user clicks 'Previous' button of the wizard.
*
*/
private void previousClicked()
{
WizardStep cStep = getCurrentStep();
application.previousClicked(cStep, this);
setCurrentStep(application.getPreviousWizardStep(cStep));
}
/**
* Method called when user clicks 'Quit' button of the wizard.
*
*/
private void quitClicked()
{
WizardStep cStep = getCurrentStep();
application.quitClicked(cStep, this);
}
/**
* Method called when user clicks 'Continue' button in the case where there
* is something installed.
*/
private void continueInstallClicked()
{
// TODO: move this stuff to Installer?
application.forceToDisplay();
getDialog().forceToDisplay();
setCurrentStep(Step.WELCOME);
}
/**
* Method called when user clicks 'Close' button of the wizard.
*
*/
private void closeClicked()
{
WizardStep cStep = getCurrentStep();
application.closeClicked(cStep, this);
}
private void launchStatusPanelClicked()
{
BackgroundTask<Object> worker = new BackgroundTask<Object>()
{
public Object processBackgroundTask() throws UserDataException {
try
{
Installation installation;
if (isWebStart()) {
String installDir = application.getUserData().getServerLocation();
installation =
new Installation(installDir, installDir);
} else {
installation = Installation.getLocal();
}
ProcessBuilder pb;
if (isMacOS()) {
ArrayList<String> cmd = new ArrayList<String>();
cmd.add(MAC_APPLICATIONS_OPENER);
cmd.add(getScriptPath(
getPath(installation.getControlPanelCommandFile())));
pb = new ProcessBuilder(cmd);
} else {
String cmd = getScriptPath(
getPath(installation.getControlPanelCommandFile()));
pb = new ProcessBuilder(cmd);
}
Map<String, String> env = pb.environment();
env.put(SetupUtils.OPENDJ_JAVA_HOME, System.getProperty("java.home"));
Process process = pb.start();
/**
* Wait for 3 seconds. Assume that if the process has not exited
* everything went fine.
*/
int returnValue = 0;
try
{
Thread.sleep(3000);
}
catch (Throwable t)
{
}
try
{
returnValue = process.exitValue();
}
catch (IllegalThreadStateException ithse)
{
// The process has not exited: assume that the status panel could
// be launched successfully.
}
if (returnValue != 0)
{
throw new Error(
INFO_COULD_NOT_LAUNCH_CONTROL_PANEL_MSG.get().toString());
}
}
catch (Throwable t)
{
// This looks like a bug
t.printStackTrace();
throw new Error(
INFO_COULD_NOT_LAUNCH_CONTROL_PANEL_MSG.get().toString());
}
return null;
}
public void backgroundTaskCompleted(Object returnValue,
Throwable throwable)
{
getDialog().getFrame().setCursor(
Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
if (throwable != null)
{
displayError(Message.raw(throwable.getMessage()),
INFO_ERROR_TITLE.get());
}
}
};
getDialog().getFrame().setCursor(
Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
worker.startBackgroundTask();
}
/**
* This method tries to update the visibility of the steps panel. The
* contents are updated because the user clicked in one of the buttons
* that could make the steps panel to change.
*
*/
private void inputPanelButtonClicked()
{
getDialog().getStepsPanel().updateStepVisibility(this);
}
/**
* Method called when we want to quit the setup (for instance when the user
* clicks on 'Close' or 'Quit' buttons and has confirmed that (s)he wants to
* quit the program.
*
*/
public void quit()
{
LOG.log(Level.INFO, "quitting application");
flushLogs();
System.exit(0);
}
private void flushLogs() {
Handler[] handlers = LOG.getHandlers();
if (handlers != null) {
for (Handler h : handlers) {
h.flush();
}
}
}
/**
* Launch the QuickSetup application Open DS.
*/
public void launch()
{
application.addProgressUpdateListener(this);
new Thread(application, "Application Thread").start();
Thread t = new Thread(new Runnable()
{
public void run()
{
runDisplayUpdater();
WizardStep ws = application.getCurrentWizardStep();
getDialog().getButtonsPanel().updateButtons(ws);
}
});
t.start();
}
/**
* Get the current step.
*
* @return the currently displayed Step of the wizard.
*/
private WizardStep getCurrentStep()
{
return currentStep;
}
/**
* Set the current step. This will basically make the required calls in the
* dialog to display the panel that corresponds to the step passed as
* argument.
*
* @param step The step to be displayed.
*/
public void setCurrentStep(WizardStep step)
{
if (step == null)
{
throw new NullPointerException("step is null");
}
currentStep = step;
application.setDisplayedWizardStep(step, application.getUserData(),
getDialog());
}
/**
* Get the dialog that is displayed.
*
* @return the dialog.
*/
public QuickSetupDialog getDialog()
{
if (dialog == null)
{
dialog = new QuickSetupDialog(application,
installStatus, this);
dialog.addButtonActionListener(this);
application.setQuickSetupDialog(dialog);
}
return dialog;
}
/**
* Displays an error message dialog.
*
* @param msg
* the error message.
* @param title
* the title for the dialog.
*/
public void displayError(Message msg, Message title)
{
if (isCli()) {
System.err.println(msg);
} else {
getDialog().displayError(msg, title);
}
}
/**
* Displays a confirmation message dialog.
*
* @param msg
* the confirmation message.
* @param title
* the title of the dialog.
* @return <CODE>true</CODE> if the user confirms the message, or
* <CODE>false</CODE> if not.
*/
public boolean displayConfirmation(Message msg, Message title)
{
return getDialog().displayConfirmation(msg, title);
}
/**
* Gets the string value for a given field name.
*
* @param fieldName
* the field name object.
* @return the string value for the field name.
*/
public String getFieldStringValue(FieldName fieldName)
{
String sValue = null;
Object value = getFieldValue(fieldName);
if (value != null)
{
if (value instanceof String)
{
sValue = (String) value;
} else
{
sValue = String.valueOf(value);
}
}
return sValue;
}
/**
* Gets the value for a given field name.
*
* @param fieldName
* the field name object.
* @return the value for the field name.
*/
public Object getFieldValue(FieldName fieldName)
{
return getDialog().getFieldValue(fieldName);
}
/**
* Marks the fieldName as valid or invalid depending on the value of the
* invalid parameter. With the current implementation this implies basically
* using a red color in the label associated with the fieldName object. The
* color/style used to mark the label invalid is specified in UIFactory.
*
* @param fieldName
* the field name object.
* @param invalid
* whether to mark the field valid or invalid.
*/
public void displayFieldInvalid(FieldName fieldName, boolean invalid)
{
getDialog().displayFieldInvalid(fieldName, invalid);
}
/**
* A method to initialize the look and feel.
*
*/
private void initLookAndFeel() throws Throwable
{
UIFactory.initialize();
}
/**
* A methods that creates an ProgressDescriptor based on the value of a
* ProgressUpdateEvent.
*
* @param ev
* the ProgressUpdateEvent used to generate the
* ProgressDescriptor.
* @return the ProgressDescriptor.
*/
private ProgressDescriptor createProgressDescriptor(ProgressUpdateEvent ev)
{
ProgressStep status = ev.getProgressStep();
Message newProgressLabel = ev.getCurrentPhaseSummary();
Message additionalDetails = ev.getNewLogs();
Integer ratio = ev.getProgressRatio();
if (additionalDetails != null)
{
progressDetails.append(additionalDetails);
}
// Note: progressDetails might have a certain number of characters that
// break Message Formatter (for instance percentages). When fix for
// issue 2142 was committed it broke this code. So here
// we use Message.raw instead of calling directly progressDetails.toMessage
return new ProgressDescriptor(status, ratio, newProgressLabel,
Message.raw(progressDetails.toString()));
}
/**
* This is a class used when the user clicks on next and that extends
* BackgroundTask.
*/
private class NextClickedBackgroundTask extends BackgroundTask<Object>
{
private WizardStep cStep;
public NextClickedBackgroundTask(WizardStep cStep)
{
this.cStep = cStep;
}
public Object processBackgroundTask() throws UserDataException {
try {
application.updateUserData(cStep, QuickSetup.this);
}
catch (UserDataException uide) {
throw uide;
}
catch (Throwable t) {
throw new UserDataException(cStep,
getThrowableMsg(INFO_BUG_MSG.get(), t));
}
return null;
}
public void backgroundTaskCompleted(Object returnValue,
Throwable throwable) {
getDialog().workerFinished();
if (throwable != null)
{
if (!(throwable instanceof UserDataException))
{
LOG.log(Level.WARNING, "Unhandled exception.", throwable);
}
else
{
UserDataException ude = (UserDataException) throwable;
if (ude instanceof UserDataConfirmationException)
{
if (displayConfirmation(ude.getMessageObject(),
INFO_CONFIRMATION_TITLE.get()))
{
setCurrentStep(application.getNextWizardStep(cStep));
}
}
else if (ude instanceof UserDataCertificateException)
{
final UserDataCertificateException ce =
(UserDataCertificateException)ude;
CertificateDialog dlg =
new CertificateDialog(getDialog().getFrame(), ce);
dlg.pack();
dlg.setVisible(true);
CertificateDialog.ReturnType answer = dlg.getUserAnswer();
if (answer != CertificateDialog.ReturnType.NOT_ACCEPTED)
{
/*
* Retry the click but now with the certificate accepted.
*/
application.acceptCertificateForException(ce,
answer == CertificateDialog.ReturnType.ACCEPTED_PERMANENTLY);
application.nextClicked(cStep, QuickSetup.this);
BackgroundTask<Object> worker =
new NextClickedBackgroundTask(cStep);
getDialog().workerStarted();
worker.startBackgroundTask();
}
}
else
{
displayError(ude.getMessageObject(), INFO_ERROR_TITLE.get());
}
}
} else {
setCurrentStep(application.getNextWizardStep(cStep));
}
}
}
}