001package apps;
002
003import apps.gui3.tabbedpreferences.TabbedPreferences;
004
005import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
006
007import java.io.*;
008import java.lang.reflect.InvocationTargetException;
009
010import javax.swing.SwingUtilities;
011
012import jmri.*;
013import jmri.jmrit.logixng.LogixNGPreferences;
014import jmri.jmrit.revhistory.FileHistory;
015import jmri.profile.Profile;
016import jmri.profile.ProfileManager;
017import jmri.script.JmriScriptEngineManager;
018import jmri.util.FileUtil;
019import jmri.util.ThreadingUtil;
020
021import jmri.util.prefs.JmriPreferencesActionFactory;
022
023import org.slf4j.Logger;
024import org.slf4j.LoggerFactory;
025
026import apps.util.Log4JUtil;
027
028/**
029 * Base class for the core of JMRI applications.
030 * <p>
031 * This provides a non-GUI base for applications. Below this is the
032 * {@link apps.gui3.Apps3} subclass which provides basic Swing GUI support.
033 * <p>
034 * There are a series of steps in the configuration:
035 * <dl>
036 * <dt>preInit<dd>Initialize log4j, invoked from the main()
037 * <dt>ctor<dd>Construct the basic application object
038 * </dl>
039 *
040 * @author Bob Jacobsen Copyright 2009, 2010
041 */
042public abstract class AppsBase {
043
044    private final static String CONFIG_FILENAME = System.getProperty("org.jmri.Apps.configFilename", "/JmriConfig3.xml");
045    protected boolean configOK;
046    protected boolean configDeferredLoadOK;
047    protected boolean preferenceFileExists;
048    static boolean preInit = false;
049    private final static Logger log = LoggerFactory.getLogger(AppsBase.class);
050
051    /**
052     * Initial actions before frame is created, invoked in the applications
053     * main() routine.
054     * <ul>
055     * <li> Initialize logging
056     * <li> Set application name
057     * </ul>
058     *
059     * @param applicationName The application name as presented to the user
060     */
061    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( value="SLF4J_FORMAT_SHOULD_BE_CONST",
062        justification="Info String always needs to be evaluated")
063    static public void preInit(String applicationName) {
064        Log4JUtil.initLogging();
065
066        try {
067            Application.setApplicationName(applicationName);
068        } catch (IllegalAccessException | IllegalArgumentException ex) {
069            log.error("Unable to set application name", ex);
070        }
071
072        log.info(Log4JUtil.startupInfo(applicationName));
073
074        preInit = true;
075    }
076
077    /**
078     * Create and initialize the application object.
079     *
080     * @param applicationName user-visible name of application
081     * @param configFileDef   default config filename
082     * @param args            arguments passed to application at launch
083     */
084    @SuppressFBWarnings(value = "SC_START_IN_CTOR",
085            justification = "The thread is only called to help improve user experiance when opening the preferences, it is not critical for it to be run at this stage")
086    public AppsBase(String applicationName, String configFileDef, String[] args) {
087
088        if (!preInit) {
089            preInit(applicationName);
090            setConfigFilename(configFileDef, args);
091        }
092
093        Log4JUtil.initLogging();
094
095        configureProfile();
096
097        installConfigurationManager();
098
099        installManagers();
100
101        setAndLoadPreferenceFile();
102
103        FileUtil.logFilePaths();
104
105        if (Boolean.getBoolean("org.jmri.python.preload")) {
106            new Thread(() -> {
107                try {
108                    JmriScriptEngineManager.getDefault().initializeAllEngines();
109                } catch (Exception ex) {
110                    log.error("Error initializing python interpreter", ex);
111                }
112            }, "initialize python interpreter").start();
113        }
114
115        // all loaded, initialize objects as necessary
116        InstanceManager.getDefault(jmri.LogixManager.class).activateAllLogixs();
117        InstanceManager.getDefault(jmri.jmrit.display.layoutEditor.LayoutBlockManager.class).initializeLayoutBlockPaths();
118
119        jmri.jmrit.logixng.LogixNG_Manager logixNG_Manager =
120                InstanceManager.getDefault(jmri.jmrit.logixng.LogixNG_Manager.class);
121        logixNG_Manager.setupAllLogixNGs();
122        if (InstanceManager.getDefault(LogixNGPreferences.class).getStartLogixNGOnStartup()
123                && InstanceManager.getDefault(jmri.jmrit.logixng.LogixNG_Manager.class).isStartLogixNGsOnLoad()) {
124            logixNG_Manager.activateAllLogixNGs();
125        }
126    }
127
128    /**
129     * Configure the {@link jmri.profile.Profile} to use for this application.
130     * <p>
131     * Note that GUI-based applications must override this method, since this
132     * method does not provide user feedback.
133     */
134    protected void configureProfile() {
135        String profileFilename;
136        FileUtil.createDirectory(FileUtil.getPreferencesPath());
137        // Load permission manager
138        InstanceManager.getDefault(PermissionManager.class);
139        // Needs to be declared final as we might need to
140        // refer to this on the Swing thread
141        File profileFile;
142        profileFilename = getConfigFileName().replaceFirst(".xml", ".properties");
143        // decide whether name is absolute or relative
144        if (!new File(profileFilename).isAbsolute()) {
145            // must be relative, but we want it to
146            // be relative to the preferences directory
147            profileFile = new File(FileUtil.getPreferencesPath() + profileFilename);
148        } else {
149            profileFile = new File(profileFilename);
150        }
151        ProfileManager.getDefault().setConfigFile(profileFile);
152        // See if the profile to use has been specified on the command line as
153        // a system property org.jmri.profile as a profile id.
154        if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) {
155            ProfileManager.getDefault().setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY));
156        }
157        // @see jmri.profile.ProfileManager#migrateToProfiles Javadoc for conditions handled here
158        if (!profileFile.exists()) { // no profile config for this app
159            try {
160                if (ProfileManager.getDefault().migrateToProfiles(getConfigFileName())) { // migration or first use
161                    // GUI should show message here
162                    log.info("Migrated {}",Bundle.getMessage("ConfigMigratedToProfile"));
163                }
164            } catch (IOException | IllegalArgumentException ex) {
165                // GUI should show message here
166                log.error("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
167            }
168        }
169        try {
170            // GUI should use ProfileManagerDialog.getStartingProfile here
171            if (ProfileManager.getStartingProfile() != null) {
172                // Manually setting the configFilename property since calling
173                // Apps.setConfigFilename() does not reset the system property
174                System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME);
175                Profile profile = ProfileManager.getDefault().getActiveProfile();
176                if (profile != null) {
177                    log.info("Starting with profile {}", profile.getId());
178                } else {
179                    log.info("Starting without a profile");
180                }
181            } else {
182                log.error("Specify profile to use as command line argument.");
183                log.error("If starting with saved profile configuration, ensure the autoStart property is set to \"true\"");
184                log.error("Profiles not configurable. Using fallback per-application configuration.");
185            }
186        } catch (IOException ex) {
187            log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
188        }
189    }
190
191    protected void installConfigurationManager() {
192        // install a Preferences Action Factory
193        InstanceManager.store(new AppsPreferencesActionFactory(), JmriPreferencesActionFactory.class);
194        ConfigureManager cm = new AppsConfigurationManager();
195        FileUtil.createDirectory(FileUtil.getUserFilesPath());
196        InstanceManager.store(cm, ConfigureManager.class);
197        InstanceManager.setDefault(ConfigureManager.class, cm);
198        log.debug("config manager installed");
199    }
200
201    protected void installManagers() {
202        // record startup
203        String appString = String.format("%s (v%s)", Application.getApplicationName(), Version.getCanonicalVersion());
204        InstanceManager.getDefault(FileHistory.class).addOperation("app", appString, null);
205
206        // install the abstract action model that allows items to be added to the, both
207        // CreateButton and Perform Action Model use a common Abstract class
208        InstanceManager.store(new CreateButtonModel(), CreateButtonModel.class);
209    }
210
211    /**
212     * Invoked to load the preferences information, and in the process configure
213     * the system. The high-level steps are:
214     * <ul>
215     * <li>Locate the preferences file based through
216     * {@link FileUtil#getFile(String)}
217     * <li>See if the preferences file exists, and handle it if it doesn't
218     * <li>Obtain a {@link jmri.ConfigureManager} from the
219     * {@link jmri.InstanceManager}
220     * <li>Ask that ConfigureManager to load the file, in the process loading
221     * information into existing and new managers.
222     * <li>Do any deferred loads that are needed
223     * <li>If needed, migrate older formats
224     * </ul>
225     * (There's additional handling for shared configurations)
226     */
227    protected void setAndLoadPreferenceFile() {
228        FileUtil.createDirectory(FileUtil.getUserFilesPath());
229        final File file;
230        File sharedConfig = null;
231        try {
232            sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG);
233            if (!sharedConfig.canRead()) {
234                sharedConfig = null;
235            }
236        } catch (FileNotFoundException ex) {
237            // ignore - this only means that sharedConfig does not exist.
238        }
239        if (sharedConfig != null) {
240            file = sharedConfig;
241        } else if (!new File(getConfigFileName()).isAbsolute()) {
242            // must be relative, but we want it to
243            // be relative to the preferences directory
244            file = new File(FileUtil.getUserFilesPath() + getConfigFileName());
245        } else {
246            file = new File(getConfigFileName());
247        }
248        // don't try to load if doesn't exist, but mark as not OK
249        if (!file.exists()) {
250            preferenceFileExists = false;
251            configOK = false;
252            log.info("No pre-existing config file found, searched for '{}'", file.getPath());
253            return;
254        }
255        preferenceFileExists = true;
256
257        // ensure the UserPreferencesManager has loaded. Done on GUI
258        // thread as it can modify GUI objects
259        ThreadingUtil.runOnGUI(() -> {
260            InstanceManager.getDefault(jmri.UserPreferencesManager.class);
261        });
262
263        // now (attempt to) load the config file
264        try {
265            ConfigureManager cm = InstanceManager.getNullableDefault(jmri.ConfigureManager.class);
266            if (cm != null) {
267                configOK = cm.load(file);
268            } else {
269                configOK = false;
270            }
271            log.debug("end load config file {}, OK={}", file.getName(), configOK);
272        } catch (JmriException e) {
273            configOK = false;
274        }
275
276        if (sharedConfig != null) {
277            // sharedConfigs do not need deferred loads
278            configDeferredLoadOK = true;
279        } else if (SwingUtilities.isEventDispatchThread()) {
280            // To avoid possible locks, deferred load should be
281            // performed on the Swing thread
282            configDeferredLoadOK = doDeferredLoad(file);
283        } else {
284            try {
285                // Use invokeAndWait method as we don't want to
286                // return until deferred load is completed
287                SwingUtilities.invokeAndWait(() -> {
288                    configDeferredLoadOK = doDeferredLoad(file);
289                });
290            } catch (InterruptedException | InvocationTargetException ex) {
291                log.error("Exception creating system console frame:", ex);
292            }
293        }
294        if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) {
295            log.info("Migrating preferences to new format...");
296            // migrate preferences
297            InstanceManager.getOptionalDefault(TabbedPreferences.class).ifPresent(tp -> {
298                //tp.init();
299                tp.saveContents();
300                InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> {
301                    cm.storePrefs();
302                });
303                // notify user of change
304                log.info("Preferences have been migrated to new format.");
305                log.info("New preferences format will be used after JMRI is restarted.");
306            });
307        }
308    }
309
310    private boolean doDeferredLoad(File file) {
311        boolean result;
312        log.debug("start deferred load from config file {}", file.getName());
313        try {
314            ConfigureManager cm = InstanceManager.getNullableDefault(jmri.ConfigureManager.class);
315            if (cm != null) {
316                result = cm.loadDeferred(file);
317            } else {
318                log.error("Failed to get default configure manager");
319                result = false;
320            }
321        } catch (JmriException e) {
322            log.error("Unhandled problem loading deferred configuration:", e);
323            result = false;
324        }
325        log.debug("end deferred load from config file {}, OK={}", file.getName(), result);
326        return result;
327    }
328
329    /**
330     * Final actions before releasing control of the application to the user,
331     * invoked explicitly after object has been constructed in main().
332     */
333    protected void start() {
334        log.debug("main initialization done");
335    }
336
337    /**
338     * Set up the configuration file name at startup.
339     * <p>
340     * The Configuration File name variable holds the name used to load the
341     * configuration file during later startup processing. Applications invoke
342     * this method to handle the usual startup hierarchy:
343     * <ul>
344     * <li>If an absolute filename was provided on the command line, use it
345     * <li>If a filename was provided that's not absolute, consider it to be in
346     * the preferences directory
347     * <li>If no filename provided, use a default name (that's application specific)
348     * </ul>
349     * This name will be used for reading and writing the preferences. It need
350     * not exist when the program first starts up. This name may be proceeded
351     * with <em>config=</em>.
352     *
353     * @param def  Default value if no other is provided
354     * @param args Argument array from the main routine
355     */
356    static protected void setConfigFilename(String def, String[] args) {
357        // skip if org.jmri.Apps.configFilename is set
358        if (System.getProperty("org.jmri.Apps.configFilename") != null) {
359            return;
360        }
361        // save the configuration filename if present on the command line
362        if (args.length >= 1 && args[0] != null && !args[0].equals("") && !args[0].contains("=")) {
363            def = args[0];
364            log.debug("Config file was specified as: {}", args[0]);
365        }
366        for (String arg : args) {
367            String[] split = arg.split("=", 2);
368            if (split[0].equalsIgnoreCase("config")) {
369                def = split[1];
370                log.debug("Config file was specified as: {}", arg);
371            }
372        }
373        if (def != null) {
374            setJmriSystemProperty("configFilename", def);
375            log.debug("Config file set to: {}", def);
376        }
377    }
378
379    // We will use the value stored in the system property
380    static public String getConfigFileName() {
381        if (System.getProperty("org.jmri.Apps.configFilename") != null) {
382            return System.getProperty("org.jmri.Apps.configFilename");
383        }
384        return CONFIG_FILENAME;
385    }
386
387    static protected void setJmriSystemProperty(String key, String value) {
388        try {
389            String current = System.getProperty("org.jmri.Apps." + key);
390            if (current == null) {
391                System.setProperty("org.jmri.Apps." + key, value);
392            } else if (!current.equals(value)) {
393                log.warn("JMRI property {} already set to {}, skipping reset to {}", key, current, value);
394            }
395        } catch (Exception e) {
396            log.error("Unable to set JMRI property {} to {}due to exception", key, value, e);
397        }
398    }
399
400    /**
401     * The application decided to quit, handle that.
402     *
403     * @return always returns false
404     */
405    static public boolean handleQuit() {
406        log.debug("Start handleQuit");
407        try {
408            InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown();
409        } catch (Exception e) {
410            log.error("Continuing after error in handleQuit", e);
411        }
412        return false;
413    }
414
415    /**
416     * The application decided to restart, handle that.
417     */
418    static public void handleRestart() {
419        log.debug("Start handleRestart");
420        try {
421            InstanceManager.getDefault(jmri.ShutDownManager.class).restart();
422        } catch (Exception e) {
423            log.error("Continuing after error in handleRestart", e);
424        }
425    }
426
427
428}