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}