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