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}