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}