001package jmri.implementation; 002 003import java.awt.GraphicsEnvironment; 004import java.awt.Toolkit; 005import java.awt.datatransfer.Clipboard; 006import java.awt.datatransfer.StringSelection; 007import java.awt.event.ActionEvent; 008import java.awt.event.ActionListener; 009import java.awt.event.KeyEvent; 010import java.io.File; 011import java.net.URISyntaxException; 012import java.net.URL; 013import java.util.*; 014import java.util.concurrent.atomic.AtomicBoolean; 015import java.util.concurrent.atomic.AtomicReference; 016 017import javax.swing.Action; 018import javax.swing.JFileChooser; 019import javax.swing.JList; 020import javax.swing.JMenuItem; 021import javax.swing.JPopupMenu; 022import javax.swing.KeyStroke; 023import javax.swing.TransferHandler; 024import javax.swing.event.ListSelectionEvent; 025 026import jmri.util.prefs.JmriPreferencesActionFactory; 027 028import jmri.Application; 029import jmri.ConfigureManager; 030import jmri.InstanceManager; 031import jmri.JmriException; 032import jmri.configurexml.ConfigXmlManager; 033import jmri.configurexml.swing.DialogErrorHandler; 034import jmri.jmrit.XmlFile; 035import jmri.jmrit.roster.RosterLocationUnavailableException; 036import jmri.profile.Profile; 037import jmri.profile.ProfileManager; 038import jmri.spi.PreferencesManager; 039import jmri.util.FileUtil; 040import jmri.util.SystemType; 041import jmri.util.com.sun.TransferActionListener; 042import jmri.util.prefs.HasConnectionButUnableToConnectException; 043import jmri.util.prefs.InitializationException; 044import jmri.util.swing.JmriJOptionPane; 045 046/** 047 * 048 * @author Randall Wood 049 */ 050public class JmriConfigurationManager implements ConfigureManager { 051 052 private final ConfigXmlManager legacy = new ConfigXmlManager(); 053 private final HashMap<PreferencesManager, InitializationException> initializationExceptions = new HashMap<>(); 054 /* 055 * This list is in order of initialization and is used to display errors in 056 * the order they appear. 057 */ 058 private final List<PreferencesManager> initialized = new ArrayList<>(); 059 /* 060 * This set is used to prevent a stack overflow by preventing 061 * initializeProvider from recursively being called with the same provider. 062 */ 063 private final Set<PreferencesManager> initializing = new HashSet<>(); 064 065 public JmriConfigurationManager() { 066 ServiceLoader<PreferencesManager> sl = ServiceLoader.load(PreferencesManager.class); 067 for (PreferencesManager pp : sl) { 068 InstanceManager.store(pp, PreferencesManager.class); 069 070 for (Class<?> provided : pp.getProvides()) { 071 InstanceManager.storeUnchecked(pp, provided); 072 } 073 074 } 075 Profile profile = ProfileManager.getDefault().getActiveProfile(); 076 if (profile != null) { 077 this.legacy.setPrefsLocation(new File(profile.getPath(), Profile.CONFIG_FILENAME)); 078 } 079 if (!GraphicsEnvironment.isHeadless()) { 080 ConfigXmlManager.setErrorHandler(new DialogErrorHandler()); 081 } 082 } 083 084 @Override 085 public void registerPref(Object o) { 086 if ((o instanceof PreferencesManager)) { 087 InstanceManager.store((PreferencesManager) o, PreferencesManager.class); 088 } 089 this.legacy.registerPref(o); 090 } 091 092 @Override 093 public void removePrefItems() { 094 this.legacy.removePrefItems(); 095 } 096 097 @Override 098 public void registerConfig(Object o) { 099 this.legacy.registerConfig(o); 100 } 101 102 @Override 103 public void registerConfig(Object o, int x) { 104 this.legacy.registerConfig(o, x); 105 } 106 107 @Override 108 public void registerTool(Object o) { 109 this.legacy.registerTool(o); 110 } 111 112 @Override 113 public void registerUser(Object o) { 114 this.legacy.registerUser(o); 115 } 116 117 @Override 118 public void registerUserPrefs(Object o) { 119 this.legacy.registerUserPrefs(o); 120 } 121 122 @Override 123 public void deregister(Object o) { 124 this.legacy.deregister(o); 125 } 126 127 @Override 128 public Object findInstance(Class<?> c, int index) { 129 return this.legacy.findInstance(c, index); 130 } 131 132 @Override 133 public List<Object> getInstanceList(Class<?> c) { 134 return this.legacy.getInstanceList(c); 135 } 136 137 /** 138 * Save preferences. Preferences are saved using either the 139 * {@link jmri.util.prefs.JmriConfigurationProvider} or 140 * {@link jmri.util.prefs.JmriPreferencesProvider} as appropriate to the 141 * register preferences handler. 142 */ 143 @Override 144 public void storePrefs() { 145 log.debug("Saving preferences..."); 146 Profile profile = ProfileManager.getDefault().getActiveProfile(); 147 InstanceManager.getList(PreferencesManager.class).stream().forEach((o) -> { 148 log.debug("Saving preferences for {}", o.getClass().getName()); 149 o.savePreferences(profile); 150 }); 151 } 152 153 /** 154 * Save preferences. This method calls {@link #storePrefs() }. 155 * 156 * @param file Ignored. 157 */ 158 @Override 159 public void storePrefs(File file) { 160 this.storePrefs(); 161 } 162 163 @Override 164 public void storeUserPrefs(File file) { 165 this.legacy.storeUserPrefs(file); 166 } 167 168 @Override 169 public boolean storeConfig(File file) { 170 return this.legacy.storeConfig(file); 171 } 172 173 @Override 174 public boolean storeUser(File file) { 175 return this.legacy.storeUser(file); 176 } 177 178 @Override 179 public boolean load(File file) throws JmriException { 180 return this.load(file, false); 181 } 182 183 @Override 184 public boolean load(URL url) throws JmriException { 185 return this.load(url, false); 186 } 187 188 @Override 189 public boolean load(File file, boolean registerDeferred) throws JmriException { 190 return this.load(FileUtil.fileToURL(file), registerDeferred); 191 } 192 193 @Override 194 public boolean load(URL url, boolean registerDeferred) throws JmriException { 195 log.debug("loading {} ...", url); 196 try { 197 if (url == null 198 || (new File(url.toURI())).getName().equals(Profile.CONFIG_FILENAME) 199 || (new File(url.toURI())).getName().equals(Profile.CONFIG)) { 200 Profile profile = ProfileManager.getDefault().getActiveProfile(); 201 List<PreferencesManager> providers = new ArrayList<>(InstanceManager.getList(PreferencesManager.class)); 202 providers.stream() 203 // sorting is a best-effort attempt to ensure that the 204 // more providers a provider relies on the later it will 205 // be initialized; this should tend to cause providers 206 // that list explicit requirements get run before providers 207 // attempting to force themselves to run last by requiring 208 // all providers 209 .sorted(Comparator.comparingInt(p -> p.getRequires().size())) 210 .forEachOrdered(provider -> initializeProvider(provider, profile)); 211 if (!this.initializationExceptions.isEmpty()) { 212 handleInitializationExceptions(profile); 213 } 214 if (url != null && (new File(url.toURI())).getName().equals(Profile.CONFIG_FILENAME)) { 215 log.debug("Loading legacy configuration..."); 216 return this.legacy.load(url, registerDeferred); 217 } 218 return this.initializationExceptions.isEmpty(); 219 } 220 } catch (URISyntaxException ex) { 221 log.error("Unable to get File for {}", url); 222 throw new JmriException(ex.getMessage(), ex); 223 } 224 // make this url the default "Store Panels..." file 225 try { 226 JFileChooser ufc = jmri.configurexml.StoreXmlUserAction.getUserFileChooser(); 227 ufc.setSelectedFile(new File(FileUtil.urlToURI(url))); 228 } catch (Exception e) { 229 // A user was seeing an IndexOutOfBoundsException in the setSelectedFile above 230 // when loading a file at startup. 231 // We don't know why, but see https://stackoverflow.com/questions/37322892/jfilechooser-java-lang-indexoutofboundsexception-invalid-index 232 // and https://web.archive.org/web/20170924021323/http://bugs.java.com/view_bug.do?bug_id=6684952 233 // This lets operation proceed past that exception. 234 log.error("Exception caught while setting default load file in file chooser: {}", e.toString()); 235 } 236 237 return this.legacy.load(url, registerDeferred); 238 // return true; // always return true once legacy support is dropped 239 } 240 241 private void handleInitializationExceptions(Profile profile) { 242 if (!GraphicsEnvironment.isHeadless()) { 243 244 AtomicBoolean isUnableToConnect = new AtomicBoolean(false); 245 AtomicReference<RosterLocationUnavailableException> rosterUnavailable = new AtomicReference<>(); 246 247 List<String> errors = new ArrayList<>(); 248 this.initialized.forEach((provider) -> { 249 List<Exception> exceptions = provider.getInitializationExceptions(profile); 250 if (!exceptions.isEmpty()) { 251 exceptions.forEach((exception) -> { 252 if (exception instanceof HasConnectionButUnableToConnectException) { 253 isUnableToConnect.set(true); 254 } 255 errors.add(exception.getLocalizedMessage()); 256 }); 257 } else if (this.initializationExceptions.get(provider) != null) { 258 InitializationException stored = this.initializationExceptions.get(provider); 259 if (stored instanceof RosterLocationUnavailableException) { 260 rosterUnavailable.compareAndSet(null, (RosterLocationUnavailableException) stored); 261 } 262 errors.add(stored.getLocalizedMessage()); 263 } 264 }); 265 266 RosterLocationUnavailableException rosterUnavailException = rosterUnavailable.get(); 267 if (rosterUnavailException != null && handleRosterLocationUnavailable(rosterUnavailException)) { 268 return; // user chose Quit; ShutDownManager will end the JVM 269 } 270 271 Object list = getErrorListObject(errors); 272 273 if (isUnableToConnect.get()) { 274 handleConnectionError(errors, list); 275 } else { 276 displayErrorListDialog(list); 277 } 278 } 279 } 280 281 /** 282 * Show a Continue/Quit dialog when the configured roster location is 283 * unavailable at startup. Returns true if the user chose Quit (in which 284 * case {@link #handleQuit()} has been invoked); returns false only if the 285 * user explicitly chose Continue. Dismissing the dialog (close button) is 286 * treated as Quit, matching the profile chooser dialog's behavior. 287 * 288 * @param ex the exception describing the unavailable roster location 289 * @return true if the user chose to quit 290 */ 291 private boolean handleRosterLocationUnavailable(RosterLocationUnavailableException ex) { 292 Object[] options = { 293 Bundle.getMessage("ErrorDialogButtonContinue"), 294 Bundle.getMessage("ErrorDialogButtonQuitProgram", Application.getApplicationName()) 295 }; 296 Object[] message = { 297 Bundle.getMessage("RosterLocationUnavailableText", ex.getUnavailablePath()), 298 Bundle.getMessage("RosterLocationUnavailablePrompt") 299 }; 300 int choice = JmriJOptionPane.showOptionDialog( 301 null, 302 message, 303 Bundle.getMessage("RosterLocationUnavailableTitle"), 304 JmriJOptionPane.DEFAULT_OPTION, 305 JmriJOptionPane.WARNING_MESSAGE, 306 null, 307 options, 308 options[0]); 309 if (choice == 0) { 310 return false; // explicit Continue 311 } 312 handleQuit(); 313 return true; 314 } 315 316 private Object getErrorListObject(List<String> errors) { 317 Object list; 318 if (errors.size() == 1) { 319 list = errors.get(0); 320 } else { 321 list = new JList<>(errors.toArray(new String[0])); 322 } 323 return list; 324 } 325 326 protected void displayErrorListDialog(Object list) { 327 JmriJOptionPane.showMessageDialog(null, 328 new Object[]{ 329 (list instanceof JList) ? Bundle.getMessage("InitExMessageListHeader") : null, 330 list, 331 "<html><br></html>", // Add a visual break between list of errors and notes // NOI18N 332 Bundle.getMessage("InitExMessageLogs"), // NOI18N 333 Bundle.getMessage("InitExMessagePrefs"), // NOI18N 334 }, 335 Bundle.getMessage("InitExMessageTitle", Application.getApplicationName()), // NOI18N 336 JmriJOptionPane.ERROR_MESSAGE); 337 InstanceManager.getDefault(JmriPreferencesActionFactory.class) 338 .getDefaultAction().actionPerformed(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"")); 339 } 340 341 /** 342 * Show a dialog with options Quit, Restart, Change profile, Edit connections 343 * @param errors the list of error messages 344 * @param list A JList or a String with error message(s) 345 */ 346 private void handleConnectionError(List<String> errors, Object list) { 347 List<String> errorList = errors; 348 349 errorList.add(" "); // blank line below errors 350 errorList.add(Bundle.getMessage("InitExMessageLogs")); 351 352 Object[] options = generateErrorDialogButtonOptions(); 353 354 if (list instanceof JList) { 355 JPopupMenu popupMenu = new JPopupMenu(); 356 JMenuItem copyMenuItem = buildCopyMenuItem((JList<?>) list); 357 popupMenu.add(copyMenuItem); 358 359 JMenuItem copyAllMenuItem = buildCopyAllMenuItem((JList<?>) list); 360 popupMenu.add(copyAllMenuItem); 361 362 ((JList<?>) list).setComponentPopupMenu(popupMenu); 363 364 ((JList<?>) list).addListSelectionListener((ListSelectionEvent e) -> copyMenuItem.setEnabled(((JList<?>)e.getSource()).getSelectedIndex() != -1)); 365 } 366 367 handleRestartSelection(getjOptionPane(list, options)); 368 369 } 370 371 // see order of generateErrorDialogButtonOptions() 372 // -1 - dialog closed, 0 - quit, 1 - continue, 2 - editconns 373 private void handleRestartSelection(int selectedValue) { 374 if (selectedValue == 0) { 375 // Exit program 376 handleQuit(); 377 378 } else if (selectedValue == 1 || selectedValue == -1 ) { 379 // Do nothing. Let the program continue 380 381 } else if (selectedValue == 2) { 382 if (isEditDialogRestart()) { 383 handleRestart(); 384 } else { 385 // Quit program 386 handleQuit(); 387 } 388 389 } else { 390 // Exit program 391 handleQuit(); 392 } 393 } 394 395 protected boolean isEditDialogRestart() { 396 return false; 397 } 398 399 protected void handleRestart() { 400 // Restart program 401 try { 402 InstanceManager.getDefault(jmri.ShutDownManager.class).restart(); 403 } catch (Exception er) { 404 log.error("Continuing after error in handleRestart", er); 405 } 406 } 407 408 409 private int getjOptionPane(Object list, Object[] options) { 410 return JmriJOptionPane.showOptionDialog( 411 null, 412 new Object[] { 413 (list instanceof JList) ? Bundle.getMessage("InitExMessageListHeader") : null, 414 list, 415 "<html><br></html>", // Add a visual break between list of errors and notes 416 Bundle.getMessage("InitExMessageLogs"), 417 Bundle.getMessage("ErrorDialogConnectLayout")}, 418 Bundle.getMessage("InitExMessageTitle", Application.getApplicationName()), 419 JmriJOptionPane.DEFAULT_OPTION, 420 JmriJOptionPane.ERROR_MESSAGE, 421 null, 422 options, 423 null); 424 } 425 426 private JMenuItem buildCopyAllMenuItem(JList<?> list) { 427 JMenuItem copyAllMenuItem = new JMenuItem(Bundle.getMessage("MenuItemCopyAll")); 428 ActionListener copyAllActionListener = (ActionEvent e) -> { 429 StringBuilder text = new StringBuilder(); 430 for (int i = 0; i < list.getModel().getSize(); i++) { 431 text.append(list.getModel().getElementAt(i).toString()); 432 text.append(System.getProperty("line.separator")); // NOI18N 433 } 434 Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 435 systemClipboard.setContents(new StringSelection(text.toString()), null); 436 }; 437 copyAllMenuItem.setActionCommand("copyAll"); // NOI18N 438 copyAllMenuItem.addActionListener(copyAllActionListener); 439 return copyAllMenuItem; 440 } 441 442 private JMenuItem buildCopyMenuItem(JList<?> list) { 443 JMenuItem copyMenuItem = new JMenuItem(Bundle.getMessage("MenuItemCopy")); 444 TransferActionListener copyActionListener = new TransferActionListener(); 445 copyMenuItem.setActionCommand((String) TransferHandler.getCopyAction().getValue(Action.NAME)); 446 copyMenuItem.addActionListener(copyActionListener); 447 if (SystemType.isMacOSX()) { 448 copyMenuItem.setAccelerator( 449 KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.META_MASK)); 450 } else { 451 copyMenuItem.setAccelerator( 452 KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK)); 453 } 454 copyMenuItem.setMnemonic(KeyEvent.VK_C); 455 copyMenuItem.setEnabled(list.getSelectedIndex() != -1); 456 return copyMenuItem; 457 } 458 459 private Object[] generateErrorDialogButtonOptions() { 460 return new Object[] { 461 Bundle.getMessage("ErrorDialogButtonQuitProgram", Application.getApplicationName()), 462 Bundle.getMessage("ErrorDialogButtonContinue"), 463 Bundle.getMessage("ErrorDialogButtonEditConnections") 464 }; 465 } 466 467 protected void handleQuit(){ 468 try { 469 InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown(); 470 } catch (Exception e) { 471 log.error("Continuing after error in handleQuit", e); 472 } 473 } 474 475 @Override 476 public boolean loadDeferred(File file) { 477 return this.legacy.loadDeferred(file); 478 } 479 480 @Override 481 public boolean loadDeferred(URL file) { 482 return this.legacy.loadDeferred(file); 483 } 484 485 @Override 486 public URL find(String filename) { 487 return this.legacy.find(filename); 488 } 489 490 @Override 491 public boolean makeBackup(File file) { 492 return this.legacy.makeBackup(file); 493 } 494 495 private void initializeProvider(PreferencesManager provider, Profile profile) { 496 if (!initializing.contains(provider) && !provider.isInitialized(profile) && !provider.isInitializedWithExceptions(profile)) { 497 initializing.add(provider); 498 log.debug("Initializing provider {}", provider.getClass()); 499 provider.getRequires() 500 .forEach(c -> InstanceManager.getList(c) 501 .forEach(p -> initializeProvider(p, profile))); 502 try { 503 provider.initialize(profile); 504 } catch (InitializationException ex) { 505 // log all initialization exceptions, but only retain for GUI display the 506 // first initialization exception for a provider 507 if (this.initializationExceptions.putIfAbsent(provider, ex) == null) { 508 log.error("Exception initializing {}: {}", provider.getClass().getName(), ex.getMessage()); 509 } else { 510 log.error("Additional exception initializing {}: {}", provider.getClass().getName(), ex.getMessage()); 511 } 512 } 513 this.initialized.add(provider); 514 log.debug("Initialized provider {}", provider.getClass()); 515 initializing.remove(provider); 516 } 517 } 518 519 public HashMap<PreferencesManager, InitializationException> getInitializationExceptions() { 520 return new HashMap<>(initializationExceptions); 521 } 522 523 @Override 524 public void setValidate(XmlFile.Validate v) { 525 legacy.setValidate(v); 526 } 527 528 @Override 529 public XmlFile.Validate getValidate() { 530 return legacy.getValidate(); 531 } 532 533 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriConfigurationManager.class); 534 535}