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