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