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}