001package jmri.util.gui; 002 003import java.awt.Font; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Enumeration; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Locale; 010import java.util.Set; 011import java.util.prefs.BackingStoreException; 012import java.util.prefs.Preferences; 013import javax.annotation.Nonnull; 014import javax.swing.ToolTipManager; 015import javax.swing.UIManager; 016import javax.swing.UIManager.LookAndFeelInfo; 017import javax.swing.UnsupportedLookAndFeelException; 018import jmri.InstanceManagerAutoDefault; 019import jmri.beans.Bean; 020import jmri.profile.Profile; 021import jmri.profile.ProfileUtils; 022import jmri.spi.PreferencesManager; 023import jmri.util.prefs.InitializationException; 024import org.openide.util.lookup.ServiceProvider; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028/** 029 * Manage GUI Look and Feel (LAF) preferences. 030 * 031 * @author Randall Wood (C) 2015, 2020 032 */ 033@ServiceProvider(service = PreferencesManager.class) 034public class GuiLafPreferencesManager extends Bean implements PreferencesManager, InstanceManagerAutoDefault { 035 036 public static final String FONT_NAME = "fontName"; 037 public static final String FONT_SIZE = "fontSize"; 038 public static final String LOCALE = "locale"; 039 public static final String LOOK_AND_FEEL = "lookAndFeel"; 040 public static final String NONSTANDARD_MOUSE_EVENT = "nonstandardMouseEvent"; 041 // Display state in bean tables as icon. 042 public static final String GRAPHIC_TABLE_STATE = "graphicTableState"; 043 // Classic OBlock editor or tabbed tables 044 public static final String OBLOCK_EDIT_TABBED = "oblockEditTabbed"; 045 public static final String VERTICAL_TOOLBAR = "verticalToolBar"; 046 public static final String SHOW_TOOL_TIP_TIME = "showToolTipDismissDelay"; 047 public static final String EDITOR_USE_OLD_LOC_SIZE = "editorUseOldLocSize"; 048 public static final String JFILECHOOSER_FORMAT = "jfilechooserformat"; 049 public static final String MAX_COMBO_ROWS = "maxComboRows"; 050 /** 051 * Smallest font size a user can set the font size to other than zero 052 * ({@value}). A font size of 0 indicates that the system default font size 053 * will be used. 054 * 055 * @see apps.GuiLafConfigPane#MIN_DISPLAYED_FONT_SIZE 056 */ 057 public static final int MIN_FONT_SIZE = 9; 058 /** 059 * Largest font size a user can set the font size to ({@value}). 060 * 061 * @see apps.GuiLafConfigPane#MAX_DISPLAYED_FONT_SIZE 062 */ 063 public static final int MAX_FONT_SIZE = 36; 064 public static final String PROP_DIRTY = "dirty"; 065 public static final String PROP_RESTARTREQUIRED = "restartRequired"; 066 public static final String DEFAULT_FONT = "List.font"; 067 068 // preferences with default values 069 private Locale locale = Locale.getDefault(); 070 private Font currentFont = null; 071 private Font defaultFont = null; 072 private int fontSize = 0; 073 private int defaultFontSize = 11; 074 private boolean nonStandardMouseEvent = false; 075 private boolean graphicTableState = false; 076 private boolean oblockEditTabbed = false; 077 private boolean editorUseOldLocSize = false; 078 private int jFileChooserFormat = 0; 079 private String lookAndFeel = UIManager.getLookAndFeel().getClass().getName(); 080 private int toolTipDismissDelay = ToolTipManager.sharedInstance().getDismissDelay(); 081 private int maxComboRows = 0; 082 private boolean dirty = false; 083 private boolean restartRequired = false; 084 085 /* 086 * Unlike most PreferencesProviders, the GUI Look & Feel preferences should 087 * be per-application instead of per-profile. 088 */ 089 private boolean initialized = false; 090 private final List<InitializationException> exceptions = new ArrayList<>(); 091 private static final Logger log = LoggerFactory.getLogger(GuiLafPreferencesManager.class); 092 093 @Override 094 public void initialize(Profile profile) throws InitializationException { 095 if (!this.initialized) { 096 Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true); 097 this.setLocale(Locale.forLanguageTag(preferences.get(LOCALE, this.getLocale().toLanguageTag()))); 098 this.setLookAndFeel(preferences.get(LOOK_AND_FEEL, this.getLookAndFeel())); 099 100 this.setDefaultFontSize(); // before we change anything 101 this.setFontSize(preferences.getInt(FONT_SIZE, this.getDefaultFontSize())); 102 if (this.getFontSize() == 0) { 103 this.setFontSize(this.getDefaultFontSize()); 104 } 105 106 this.setFontByName(preferences.get(FONT_NAME, this.getDefaultFont().getFontName())); 107 if (this.getFont() == null) { 108 this.setFont(this.getDefaultFont()); 109 } 110 111 this.setNonStandardMouseEvent( 112 preferences.getBoolean(NONSTANDARD_MOUSE_EVENT, this.isNonStandardMouseEvent())); 113 this.setGraphicTableState(preferences.getBoolean(GRAPHIC_TABLE_STATE, this.isGraphicTableState())); 114 this.setOblockEditTabbed(preferences.getBoolean(OBLOCK_EDIT_TABBED, this.isOblockEditTabbed())); 115 this.setEditorUseOldLocSize(preferences.getBoolean(EDITOR_USE_OLD_LOC_SIZE, this.isEditorUseOldLocSize())); 116 this.setJFileChooserFormat(preferences.getInt(JFILECHOOSER_FORMAT, this.getJFileChooserFormat())); 117 this.setMaxComboRows(preferences.getInt(MAX_COMBO_ROWS, this.getMaxComboRows())); 118 this.setToolTipDismissDelay(preferences.getInt(SHOW_TOOL_TIP_TIME, this.getToolTipDismissDelay())); 119 120 log.debug("About to setDefault Locale"); 121 Locale.setDefault(this.getLocale()); 122 javax.swing.JComponent.setDefaultLocale(this.getLocale()); 123 124 this.applyLookAndFeel(); 125 this.applyFontSize(); 126 this.initialized = true; 127 } 128 } 129 130 @Override 131 public boolean isInitialized(Profile profile) { 132 return this.initialized && this.exceptions.isEmpty(); 133 } 134 135 @Override 136 @Nonnull 137 public Collection<Class<? extends PreferencesManager>> getRequires() { 138 return new HashSet<>(); 139 } 140 141 @Override 142 @Nonnull 143 public Iterable<Class<?>> getProvides() { 144 Set<Class<?>> provides = new HashSet<>(); 145 provides.add(this.getClass()); 146 return provides; 147 } 148 149 @Override 150 public void savePreferences(Profile profile) { 151 Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true); 152 preferences.put(LOCALE, this.getLocale().toLanguageTag()); 153 preferences.put(LOOK_AND_FEEL, this.getLookAndFeel()); 154 155 if (currentFont == null) { 156 currentFont = this.getDefaultFont(); 157 } 158 159 String currentFontName = currentFont.getFontName(); 160 if (currentFontName != null) { 161 String prefFontName = preferences.get(FONT_NAME, currentFontName); 162 if ((prefFontName == null) || (!prefFontName.equals(currentFontName))) { 163 preferences.put(FONT_NAME, currentFontName); 164 } 165 } 166 167 int temp = this.getFontSize(); 168 if (temp == this.getDefaultFontSize()) { 169 temp = 0; 170 } 171 if (temp != preferences.getInt(FONT_SIZE, -1)) { 172 preferences.putInt(FONT_SIZE, temp); 173 } 174 preferences.putBoolean(NONSTANDARD_MOUSE_EVENT, this.isNonStandardMouseEvent()); 175 preferences.putBoolean(GRAPHIC_TABLE_STATE, this.isGraphicTableState()); 176 preferences.putBoolean(OBLOCK_EDIT_TABBED, this.isOblockEditTabbed()); 177 preferences.putBoolean(EDITOR_USE_OLD_LOC_SIZE, this.isEditorUseOldLocSize()); 178 preferences.putInt(JFILECHOOSER_FORMAT, this.jFileChooserFormat); 179 preferences.putInt(MAX_COMBO_ROWS, this.getMaxComboRows()); 180 preferences.putInt(SHOW_TOOL_TIP_TIME, this.getToolTipDismissDelay()); 181 try { 182 preferences.sync(); 183 } catch (BackingStoreException ex) { 184 log.error("Unable to save preferences.", ex); 185 } 186 this.setDirty(false); 187 } 188 189 /** 190 * @return the locale 191 */ 192 public Locale getLocale() { 193 return locale; 194 } 195 196 /** 197 * @param locale the locale to set 198 */ 199 public void setLocale(Locale locale) { 200 Locale oldLocale = this.locale; 201 this.locale = locale; 202 firePropertyChange(LOCALE, oldLocale, locale); 203 } 204 205 /** 206 * @return the currently selected font 207 */ 208 public Font getFont() { 209 return currentFont; 210 } 211 212 /** 213 * Sets a new font 214 * 215 * @param newFont the new font to set 216 */ 217 public void setFont(Font newFont) { 218 Font oldFont = this.currentFont; 219 this.currentFont = newFont; 220 firePropertyChange(FONT_NAME, oldFont, this.currentFont); 221 } 222 223 /** 224 * Sets a new font by name 225 * 226 * @param newFontName the name of the new font to set 227 */ 228 public void setFontByName(String newFontName) { 229 Font oldFont = getFont(); 230 if (oldFont == null) { 231 oldFont = this.getDefaultFont(); 232 } 233 setFont(new Font(newFontName, oldFont.getStyle(), fontSize)); 234 } 235 236 /** 237 * @return the current Look and Feel default font 238 */ 239 public Font getDefaultFont() { 240 if (defaultFont == null) { 241 setDefaultFont(); 242 } 243 return defaultFont; 244 } 245 246 /** 247 * Called to load the current Look and Feel default font, based on 248 * looking up the {@value #DEFAULT_FONT}. 249 */ 250 public void setDefaultFont() { 251 java.util.Enumeration<Object> keys = UIManager.getDefaults().keys(); 252 while (keys.hasMoreElements()) { 253 Object key = keys.nextElement(); 254 Object value = UIManager.get(key); 255 256 if (value instanceof javax.swing.plaf.FontUIResource && key.toString().equals(DEFAULT_FONT)) { 257 Font f = UIManager.getFont(key); 258 log.debug("Key:{} Font: {}", key, f.getName()); 259 defaultFont = f; 260 return; 261 } 262 } 263 // couldn't find the default return a reasonable font 264 defaultFont = UIManager.getFont(DEFAULT_FONT); 265 if (defaultFont == null) { 266 // or maybe not quite as reasonable 267 defaultFont = UIManager.getFont("TextArea.font"); 268 } 269 } 270 271 /** 272 * @return the currently selected font size 273 */ 274 public int getFontSize() { 275 if (fontSize == 0) { 276 return defaultFontSize; 277 } 278 return fontSize; 279 } 280 281 /** 282 * Set the new font size. If newFontSize is non-zero and less than 283 * {@value #MIN_FONT_SIZE}, the font size is set to {@value #MIN_FONT_SIZE} 284 * or if greater than {@value #MAX_FONT_SIZE}, the font size is set to 285 * {@value #MAX_FONT_SIZE}. 286 * 287 * @param newFontSize the new font size to set 288 */ 289 public void setFontSize(int newFontSize) { 290 int oldFontSize = this.fontSize; 291 if (newFontSize != 0 && newFontSize < MIN_FONT_SIZE) { 292 this.fontSize = MIN_FONT_SIZE; 293 } else if (newFontSize > MAX_FONT_SIZE) { 294 this.fontSize = MAX_FONT_SIZE; 295 } else { 296 this.fontSize = newFontSize; 297 } 298 firePropertyChange(FONT_SIZE, oldFontSize, this.fontSize); 299 } 300 301 /** 302 * Get the default font size for the current Look and Feel. 303 * 304 * @return the default font size 305 */ 306 public int getDefaultFontSize() { 307 return defaultFontSize; 308 } 309 310 /** 311 * Get the default font size for the current Look and Feel, based 312 * on looking up the {@value #DEFAULT_FONT} size. 313 */ 314 public void setDefaultFontSize() { 315 java.util.Enumeration<Object> keys = UIManager.getDefaults().keys(); 316 while (keys.hasMoreElements()) { 317 Object key = keys.nextElement(); 318 Object value = UIManager.get(key); 319 320 if (value instanceof javax.swing.plaf.FontUIResource && key.toString().equals(DEFAULT_FONT)) { 321 Font f = UIManager.getFont(key); 322 log.debug("Key:{} Font: {} size: {}", key, f.getName(), f.getSize()); 323 defaultFontSize = f.getSize(); 324 return; 325 } 326 } 327 defaultFontSize = 11; // couldn't find the default return a reasonable 328 // font size 329 } 330 331 /** 332 * Logs LAF fonts at the TRACE level. 333 */ 334 private void logAllFonts() { 335 // avoid any activity if logging at this level is disabled to avoid 336 // the unnecessary overhead of getting the fonts 337 if (log.isTraceEnabled()) { 338 log.trace("******** LAF={}", UIManager.getLookAndFeel().getClass().getName()); 339 java.util.Enumeration<Object> keys = UIManager.getDefaults().keys(); 340 while (keys.hasMoreElements()) { 341 Object key = keys.nextElement(); 342 Object value = UIManager.get(key); 343 if (value != null && 344 (value instanceof javax.swing.plaf.FontUIResource || 345 value instanceof java.awt.Font || 346 key.toString().endsWith(".font"))) { 347 Font f = UIManager.getFont(key); 348 log.trace("Class={}; Key: {} Font: {} size: {}", value.getClass().getName(), key, f.getName(), 349 f.getSize()); 350 } 351 } 352 } 353 } 354 355 /** 356 * Sets the time a tooltip is displayed before it goes away. 357 * <p> 358 * Note that this preference takes effect immediately. 359 * 360 * @param time the delay in seconds. 361 */ 362 public void setToolTipDismissDelay(int time) { 363 int old = this.toolTipDismissDelay; 364 this.toolTipDismissDelay = time; 365 ToolTipManager.sharedInstance().setDismissDelay(time); 366 firePropertyChange(SHOW_TOOL_TIP_TIME, old, time); 367 } 368 369 /** 370 * Get the time a tooltip is displayed before being dismissed. 371 * 372 * @return the delay in seconds 373 */ 374 public int getToolTipDismissDelay() { 375 return this.toolTipDismissDelay; 376 } 377 378 /** 379 * @return the nonStandardMouseEvent 380 */ 381 public boolean isNonStandardMouseEvent() { 382 return nonStandardMouseEvent; 383 } 384 385 /** 386 * @param nonStandardMouseEvent the nonStandardMouseEvent to set 387 */ 388 public void setNonStandardMouseEvent(boolean nonStandardMouseEvent) { 389 boolean oldNonStandardMouseEvent = this.nonStandardMouseEvent; 390 this.nonStandardMouseEvent = nonStandardMouseEvent; 391 firePropertyChange(NONSTANDARD_MOUSE_EVENT, oldNonStandardMouseEvent, nonStandardMouseEvent); 392 } 393 394 /** 395 * @return the graphicTableState 396 */ 397 public boolean isGraphicTableState() { 398 return graphicTableState; 399 } 400 401 /** 402 * @param graphicTableState the graphicTableState to set 403 */ 404 public void setGraphicTableState(boolean graphicTableState) { 405 boolean oldGraphicTableState = this.graphicTableState; 406 this.graphicTableState = graphicTableState; 407 firePropertyChange(GRAPHIC_TABLE_STATE, oldGraphicTableState, graphicTableState); 408 } 409 410 /** 411 * @return the graphicTableState 412 */ 413 public boolean isOblockEditTabbed() { 414 return oblockEditTabbed; 415 } 416 417 /** 418 * @param tabbed the Editor interface to set (fasle = desktop) 419 */ 420 public void setOblockEditTabbed(boolean tabbed) { 421 boolean oldOblockTabbed = this.oblockEditTabbed; 422 this.oblockEditTabbed = tabbed; 423 firePropertyChange(OBLOCK_EDIT_TABBED, oldOblockTabbed, tabbed); 424 } 425 426 /** 427 * @return the number of combo box rows to be displayed. 428 */ 429 public int getMaxComboRows() { 430 return maxComboRows; 431 } 432 433 /** 434 * Set a new value for the number of combo box rows to be displayed. 435 * @param maxRows The new value, zero for no limit 436 */ 437 public void setMaxComboRows(int maxRows) { 438 maxComboRows = maxRows; 439 } 440 441 /** 442 * @return the editorUseOldLocSize value 443 */ 444 public boolean isEditorUseOldLocSize() { 445 return editorUseOldLocSize; 446 } 447 448 /** 449 * @param editorUseOldLocSize the editorUseOldLocSize value to set 450 */ 451 public void setEditorUseOldLocSize(boolean editorUseOldLocSize) { 452 boolean oldEditorUseOldLocSize = this.editorUseOldLocSize; 453 this.editorUseOldLocSize = editorUseOldLocSize; 454 firePropertyChange(EDITOR_USE_OLD_LOC_SIZE, oldEditorUseOldLocSize, editorUseOldLocSize); 455 } 456 457 /** 458 * JFileChooser Type 459 * @return 0 default, 1 List 2 Detail 460 */ 461 public int getJFileChooserFormat() { 462 return jFileChooserFormat; 463 } 464 465 /** 466 * @param jFileChooserFormat the JFileChooser 0 default, 1 list, 2 detail 467 */ 468 public void setJFileChooserFormat( int jFileChooserFormat) { 469 int oldjFileChooserFormat = this.jFileChooserFormat; 470 this.jFileChooserFormat = jFileChooserFormat; 471 firePropertyChange(JFILECHOOSER_FORMAT, oldjFileChooserFormat, jFileChooserFormat); 472 } 473 474 /** 475 * Get the name of the class implementing the preferred look and feel. Note 476 * this may not be the in-use look and feel if the preferred look and feel 477 * is not available on the current platform; and will be overwritten if 478 * preferences are saved on a platform where the preferred look and feel is 479 * not available. 480 * 481 * @return the look and feel class name 482 */ 483 public String getLookAndFeel() { 484 return lookAndFeel; 485 } 486 487 /** 488 * Set the name of the class implementing the preferred look and feel. Note 489 * this change only takes effect after the application is restarted, because 490 * Java has some issues setting the look and feel correctly on already open 491 * windows. 492 * 493 * @param lookAndFeel the look and feel class name 494 */ 495 public void setLookAndFeel(String lookAndFeel) { 496 String oldLookAndFeel = this.lookAndFeel; 497 this.lookAndFeel = lookAndFeel; 498 firePropertyChange(LOOK_AND_FEEL, oldLookAndFeel, lookAndFeel); 499 } 500 501 /** 502 * Apply the existing look and feel. 503 */ 504 public void applyLookAndFeel() { 505 String lafClassName = null; 506 for (LookAndFeelInfo LAF : UIManager.getInstalledLookAndFeels()) { 507 // accept either name or classname of look and feel 508 if (LAF.getClassName().equals(this.lookAndFeel) || LAF.getName().equals(this.lookAndFeel)) { 509 lafClassName = LAF.getClassName(); 510 break; // use first match, not last match (unlikely to be 511 // different, but you never know) 512 } 513 } 514 log.debug("Look and feel selection \"{}\" ({})", this.lookAndFeel, lafClassName); 515 if (lafClassName != null) { 516 if (!lafClassName.equals(UIManager.getLookAndFeel().getClass().getName())) { 517 log.debug("Apply look and feel \"{}\" ({})", this.lookAndFeel, lafClassName); 518 try { 519 UIManager.setLookAndFeel(lafClassName); 520 } catch (ClassNotFoundException ex) { 521 log.error("Could not find look and feel \"{}\".", this.lookAndFeel); 522 } catch ( 523 IllegalAccessException | 524 InstantiationException ex) { 525 log.error("Could not load look and feel \"{}\".", this.lookAndFeel); 526 } catch (UnsupportedLookAndFeelException ex) { 527 log.error("Look and feel \"{}\" is not supported on this platform.", this.lookAndFeel); 528 } 529 } else { 530 log.debug("Not updating look and feel {} matching existing look and feel", lafClassName); 531 } 532 } 533 } 534 535 /** 536 * Applies a new calculated font size to all found fonts. 537 * <p> 538 * Calls {@link #getCalcFontSize(int) getCalcFontSize} to calculate new size 539 * for each. 540 */ 541 private void applyFontSize() { 542 if (log.isTraceEnabled()) { 543 logAllFonts(); 544 } 545 if (this.getFontSize() != this.getDefaultFontSize()) { 546 Enumeration<Object> keys = UIManager.getDefaults().keys(); 547 while (keys.hasMoreElements()) { 548 Object key = keys.nextElement(); 549 Object value = UIManager.get(key); 550 if (value != null && 551 (value instanceof javax.swing.plaf.FontUIResource || 552 value instanceof java.awt.Font || 553 key.toString().endsWith(".font"))) { 554 UIManager.put(key, UIManager.getFont(key).deriveFont(((Font) value).getStyle(), 555 getCalcFontSize(((Font) value).getSize()))); 556 } 557 } 558 if (log.isTraceEnabled()) { 559 logAllFonts(); 560 } 561 } 562 } 563 564 /** 565 * Stand-alone service routine to set the default Locale. 566 * <p> 567 * Intended to be invoked early, as soon as a profile is available, to 568 * ensure the correct language is set as startup proceeds. Must be followed 569 * eventually by a complete {@link #setLocale}. 570 * 571 * @param profile The profile to get the locale from 572 */ 573 @SuppressWarnings("deprecation") // The constructor Locale(String) is deprecated since version 19 574 // The replacement Locale.of(String) isn't available before version 19 575 public static void setLocaleMinimally(Profile profile) { 576 // en is default if a locale preference has not been set 577 String name = ProfileUtils.getPreferences(profile, GuiLafPreferencesManager.class, true).get("locale", "en"); 578 log.debug("setLocaleMinimally found language {}, setting", name); 579 Locale.setDefault(new Locale(name)); 580 javax.swing.JComponent.setDefaultLocale(new Locale(name)); 581 } 582 583 /** 584 * @return a new calculated font size based on difference between default 585 * size and selected size 586 * @param oldSize the old font size 587 */ 588 private int getCalcFontSize(int oldSize) { 589 return oldSize + (this.getFontSize() - this.getDefaultFontSize()); 590 } 591 592 /** 593 * Check if preferences need to be saved. 594 * 595 * @return true if preferences need to be saved 596 */ 597 public boolean isDirty() { 598 return dirty; 599 } 600 601 /** 602 * Set dirty state. 603 * 604 * @param dirty true if preferences need to be saved 605 */ 606 private void setDirty(boolean dirty) { 607 if (this.initialized) { 608 boolean oldDirty = this.dirty; 609 this.dirty = dirty; 610 super.firePropertyChange(PROP_DIRTY, oldDirty, dirty); 611 } 612 } 613 614 /** 615 * Check if application needs to restart to apply preferences. 616 * 617 * @return true if preferences are only applied on application start 618 */ 619 public boolean isRestartRequired() { 620 return restartRequired; 621 } 622 623 /** 624 * Set restart required state. Sets the state to true if 625 * {@link #isInitialized(jmri.profile.Profile)} is true. 626 */ 627 private void setRestartRequired() { 628 if (initialized && !restartRequired) { 629 restartRequired = true; 630 super.firePropertyChange(PROP_RESTARTREQUIRED, false, restartRequired); 631 } 632 } 633 634 /** 635 * {@inheritDoc} 636 */ 637 @Override 638 public void firePropertyChange(String propertyName, boolean oldValue, boolean newValue) { 639 if (oldValue != newValue) { 640 setDirty(true); 641 setRestartRequired(); 642 super.firePropertyChange(propertyName, oldValue, newValue); 643 } 644 } 645 646 /** 647 * {@inheritDoc} 648 */ 649 @Override 650 public void firePropertyChange(String propertyName, int oldValue, int newValue) { 651 if (oldValue != newValue) { 652 setDirty(true); 653 setRestartRequired(); 654 super.firePropertyChange(propertyName, oldValue, newValue); 655 } 656 } 657 658 /** 659 * {@inheritDoc} 660 */ 661 @Override 662 public void firePropertyChange(String propertyName, Object oldValue, Object newValue) { 663 if (oldValue == null || newValue == null || oldValue != newValue) { 664 setDirty(true); 665 setRestartRequired(); 666 super.firePropertyChange(propertyName, oldValue, newValue); 667 } 668 } 669 670 @Override 671 public boolean isInitializedWithExceptions(Profile profile) { 672 return this.initialized && !this.exceptions.isEmpty(); 673 } 674 675 @Override 676 @Nonnull 677 public List<Exception> getInitializationExceptions(Profile profile) { 678 return new ArrayList<>(this.exceptions); 679 } 680 681}