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