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 JFileChooser ufc = jmri.configurexml.StoreXmlUserAction.getUserFileChooser(); 224 ufc.setSelectedFile(new File(FileUtil.urlToURI(url))); 225 226 return this.legacy.load(url, registerDeferred); 227 // return true; // always return true once legacy support is dropped 228 } 229 230 private void handleInitializationExceptions(Profile profile) { 231 if (!GraphicsEnvironment.isHeadless()) { 232 233 AtomicBoolean isUnableToConnect = new AtomicBoolean(false); 234 235 List<String> errors = new ArrayList<>(); 236 this.initialized.forEach((provider) -> { 237 List<Exception> exceptions = provider.getInitializationExceptions(profile); 238 if (!exceptions.isEmpty()) { 239 exceptions.forEach((exception) -> { 240 if (exception instanceof HasConnectionButUnableToConnectException) { 241 isUnableToConnect.set(true); 242 } 243 errors.add(exception.getLocalizedMessage()); 244 }); 245 } else if (this.initializationExceptions.get(provider) != null) { 246 errors.add(this.initializationExceptions.get(provider).getLocalizedMessage()); 247 } 248 }); 249 Object list = getErrorListObject(errors); 250 251 if (isUnableToConnect.get()) { 252 handleConnectionError(errors, list); 253 } else { 254 displayErrorListDialog(list); 255 } 256 } 257 } 258 259 private Object getErrorListObject(List<String> errors) { 260 Object list; 261 if (errors.size() == 1) { 262 list = errors.get(0); 263 } else { 264 list = new JList<>(errors.toArray(new String[0])); 265 } 266 return list; 267 } 268 269 protected void displayErrorListDialog(Object list) { 270 JmriJOptionPane.showMessageDialog(null, 271 new Object[]{ 272 (list instanceof JList) ? Bundle.getMessage("InitExMessageListHeader") : null, 273 list, 274 "<html><br></html>", // Add a visual break between list of errors and notes // NOI18N 275 Bundle.getMessage("InitExMessageLogs"), // NOI18N 276 Bundle.getMessage("InitExMessagePrefs"), // NOI18N 277 }, 278 Bundle.getMessage("InitExMessageTitle", Application.getApplicationName()), // NOI18N 279 JmriJOptionPane.ERROR_MESSAGE); 280 InstanceManager.getDefault(JmriPreferencesActionFactory.class) 281 .getDefaultAction().actionPerformed(new ActionEvent(this,ActionEvent.ACTION_PERFORMED,"")); 282 } 283 284 /** 285 * Show a dialog with options Quit, Restart, Change profile, Edit connections 286 * @param errors the list of error messages 287 * @param list A JList or a String with error message(s) 288 */ 289 private void handleConnectionError(List<String> errors, Object list) { 290 List<String> errorList = errors; 291 292 errorList.add(" "); // blank line below errors 293 errorList.add(Bundle.getMessage("InitExMessageLogs")); 294 295 Object[] options = generateErrorDialogButtonOptions(); 296 297 if (list instanceof JList) { 298 JPopupMenu popupMenu = new JPopupMenu(); 299 JMenuItem copyMenuItem = buildCopyMenuItem((JList<?>) list); 300 popupMenu.add(copyMenuItem); 301 302 JMenuItem copyAllMenuItem = buildCopyAllMenuItem((JList<?>) list); 303 popupMenu.add(copyAllMenuItem); 304 305 ((JList<?>) list).setComponentPopupMenu(popupMenu); 306 307 ((JList<?>) list).addListSelectionListener((ListSelectionEvent e) -> copyMenuItem.setEnabled(((JList<?>)e.getSource()).getSelectedIndex() != -1)); 308 } 309 310 handleRestartSelection(getjOptionPane(list, options)); 311 312 } 313 314 // see order of generateErrorDialogButtonOptions() 315 // -1 - dialog closed, 0 - quit, 1 - continue, 2 - editconns 316 private void handleRestartSelection(int selectedValue) { 317 if (selectedValue == 0) { 318 // Exit program 319 handleQuit(); 320 321 } else if (selectedValue == 1 || selectedValue == -1 ) { 322 // Do nothing. Let the program continue 323 324 } else if (selectedValue == 2) { 325 if (isEditDialogRestart()) { 326 handleRestart(); 327 } else { 328 // Quit program 329 handleQuit(); 330 } 331 332 } else { 333 // Exit program 334 handleQuit(); 335 } 336 } 337 338 protected boolean isEditDialogRestart() { 339 return false; 340 } 341 342 protected void handleRestart() { 343 // Restart program 344 try { 345 InstanceManager.getDefault(jmri.ShutDownManager.class).restart(); 346 } catch (Exception er) { 347 log.error("Continuing after error in handleRestart", er); 348 } 349 } 350 351 352 private int getjOptionPane(Object list, Object[] options) { 353 return JmriJOptionPane.showOptionDialog( 354 null, 355 new Object[] { 356 (list instanceof JList) ? Bundle.getMessage("InitExMessageListHeader") : null, 357 list, 358 "<html><br></html>", // Add a visual break between list of errors and notes 359 Bundle.getMessage("InitExMessageLogs"), 360 Bundle.getMessage("ErrorDialogConnectLayout")}, 361 Bundle.getMessage("InitExMessageTitle", Application.getApplicationName()), 362 JmriJOptionPane.DEFAULT_OPTION, 363 JmriJOptionPane.ERROR_MESSAGE, 364 null, 365 options, 366 null); 367 } 368 369 private JMenuItem buildCopyAllMenuItem(JList<?> list) { 370 JMenuItem copyAllMenuItem = new JMenuItem(Bundle.getMessage("MenuItemCopyAll")); 371 ActionListener copyAllActionListener = (ActionEvent e) -> { 372 StringBuilder text = new StringBuilder(); 373 for (int i = 0; i < list.getModel().getSize(); i++) { 374 text.append(list.getModel().getElementAt(i).toString()); 375 text.append(System.getProperty("line.separator")); // NOI18N 376 } 377 Clipboard systemClipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 378 systemClipboard.setContents(new StringSelection(text.toString()), null); 379 }; 380 copyAllMenuItem.setActionCommand("copyAll"); // NOI18N 381 copyAllMenuItem.addActionListener(copyAllActionListener); 382 return copyAllMenuItem; 383 } 384 385 private JMenuItem buildCopyMenuItem(JList<?> list) { 386 JMenuItem copyMenuItem = new JMenuItem(Bundle.getMessage("MenuItemCopy")); 387 TransferActionListener copyActionListener = new TransferActionListener(); 388 copyMenuItem.setActionCommand((String) TransferHandler.getCopyAction().getValue(Action.NAME)); 389 copyMenuItem.addActionListener(copyActionListener); 390 if (SystemType.isMacOSX()) { 391 copyMenuItem.setAccelerator( 392 KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.META_MASK)); 393 } else { 394 copyMenuItem.setAccelerator( 395 KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK)); 396 } 397 copyMenuItem.setMnemonic(KeyEvent.VK_C); 398 copyMenuItem.setEnabled(list.getSelectedIndex() != -1); 399 return copyMenuItem; 400 } 401 402 private Object[] generateErrorDialogButtonOptions() { 403 return new Object[] { 404 Bundle.getMessage("ErrorDialogButtonQuitProgram", Application.getApplicationName()), 405 Bundle.getMessage("ErrorDialogButtonContinue"), 406 Bundle.getMessage("ErrorDialogButtonEditConnections") 407 }; 408 } 409 410 protected void handleQuit(){ 411 try { 412 InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown(); 413 } catch (Exception e) { 414 log.error("Continuing after error in handleQuit", e); 415 } 416 } 417 418 @Override 419 public boolean loadDeferred(File file) { 420 return this.legacy.loadDeferred(file); 421 } 422 423 @Override 424 public boolean loadDeferred(URL file) { 425 return this.legacy.loadDeferred(file); 426 } 427 428 @Override 429 public URL find(String filename) { 430 return this.legacy.find(filename); 431 } 432 433 @Override 434 public boolean makeBackup(File file) { 435 return this.legacy.makeBackup(file); 436 } 437 438 private void initializeProvider(PreferencesManager provider, Profile profile) { 439 if (!initializing.contains(provider) && !provider.isInitialized(profile) && !provider.isInitializedWithExceptions(profile)) { 440 initializing.add(provider); 441 log.debug("Initializing provider {}", provider.getClass()); 442 provider.getRequires() 443 .forEach(c -> InstanceManager.getList(c) 444 .forEach(p -> initializeProvider(p, profile))); 445 try { 446 provider.initialize(profile); 447 } catch (InitializationException ex) { 448 // log all initialization exceptions, but only retain for GUI display the 449 // first initialization exception for a provider 450 if (this.initializationExceptions.putIfAbsent(provider, ex) == null) { 451 log.error("Exception initializing {}: {}", provider.getClass().getName(), ex.getMessage()); 452 } else { 453 log.error("Additional exception initializing {}: {}", provider.getClass().getName(), ex.getMessage()); 454 } 455 } 456 this.initialized.add(provider); 457 log.debug("Initialized provider {}", provider.getClass()); 458 initializing.remove(provider); 459 } 460 } 461 462 public HashMap<PreferencesManager, InitializationException> getInitializationExceptions() { 463 return new HashMap<>(initializationExceptions); 464 } 465 466 @Override 467 public void setValidate(XmlFile.Validate v) { 468 legacy.setValidate(v); 469 } 470 471 @Override 472 public XmlFile.Validate getValidate() { 473 return legacy.getValidate(); 474 } 475 476 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(JmriConfigurationManager.class); 477 478}