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        // Needs to be declared final as we might need to
138        // refer to this on the Swing thread
139        File profileFile;
140        profileFilename = getConfigFileName().replaceFirst(".xml", ".properties");
141        // decide whether name is absolute or relative
142        if (!new File(profileFilename).isAbsolute()) {
143            // must be relative, but we want it to
144            // be relative to the preferences directory
145            profileFile = new File(FileUtil.getPreferencesPath() + profileFilename);
146        } else {
147            profileFile = new File(profileFilename);
148        }
149        ProfileManager.getDefault().setConfigFile(profileFile);
150        // See if the profile to use has been specified on the command line as
151        // a system property org.jmri.profile as a profile id.
152        if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) {
153            ProfileManager.getDefault().setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY));
154        }
155        // @see jmri.profile.ProfileManager#migrateToProfiles Javadoc for conditions handled here
156        if (!profileFile.exists()) { // no profile config for this app
157            try {
158                if (ProfileManager.getDefault().migrateToProfiles(getConfigFileName())) { // migration or first use
159                    // GUI should show message here
160                    log.info("Migrated {}",Bundle.getMessage("ConfigMigratedToProfile"));
161                }
162            } catch (IOException | IllegalArgumentException ex) {
163                // GUI should show message here
164                log.error("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
165            }
166        }
167        try {
168            // GUI should use ProfileManagerDialog.getStartingProfile here
169            if (ProfileManager.getStartingProfile() != null) {
170                // Manually setting the configFilename property since calling
171                // Apps.setConfigFilename() does not reset the system property
172                System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME);
173                Profile profile = ProfileManager.getDefault().getActiveProfile();
174                if (profile != null) {
175                    log.info("Starting with profile {}", profile.getId());
176                } else {
177                    log.info("Starting without a profile");
178                }
179            } else {
180                log.error("Specify profile to use as command line argument.");
181                log.error("If starting with saved profile configuration, ensure the autoStart property is set to \"true\"");
182                log.error("Profiles not configurable. Using fallback per-application configuration.");
183            }
184        } catch (IOException ex) {
185            log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
186        }
187    }
188
189    protected void installConfigurationManager() {
190        // install a Preferences Action Factory
191        InstanceManager.store(new AppsPreferencesActionFactory(), JmriPreferencesActionFactory.class);
192        ConfigureManager cm = new AppsConfigurationManager();
193        FileUtil.createDirectory(FileUtil.getUserFilesPath());
194        InstanceManager.store(cm, ConfigureManager.class);
195        InstanceManager.setDefault(ConfigureManager.class, cm);
196        log.debug("config manager installed");
197    }
198
199    protected void installManagers() {
200        // record startup
201        String appString = String.format("%s (v%s)", Application.getApplicationName(), Version.getCanonicalVersion());
202        InstanceManager.getDefault(FileHistory.class).addOperation("app", appString, null);
203
204        // install the abstract action model that allows items to be added to the, both
205        // CreateButton and Perform Action Model use a common Abstract class
206        InstanceManager.store(new CreateButtonModel(), CreateButtonModel.class);
207    }
208
209    /**
210     * Invoked to load the preferences information, and in the process configure
211     * the system. The high-level steps are:
212     * <ul>
213     * <li>Locate the preferences file based through
214     * {@link FileUtil#getFile(String)}
215     * <li>See if the preferences file exists, and handle it if it doesn't
216     * <li>Obtain a {@link jmri.ConfigureManager} from the
217     * {@link jmri.InstanceManager}
218     * <li>Ask that ConfigureManager to load the file, in the process loading
219     * information into existing and new managers.
220     * <li>Do any deferred loads that are needed
221     * <li>If needed, migrate older formats
222     * </ul>
223     * (There's additional handling for shared configurations)
224     */
225    protected void setAndLoadPreferenceFile() {
226        FileUtil.createDirectory(FileUtil.getUserFilesPath());
227        final File file;
228        File sharedConfig = null;
229        try {
230            sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG);
231            if (!sharedConfig.canRead()) {
232                sharedConfig = null;
233            }
234        } catch (FileNotFoundException ex) {
235            // ignore - this only means that sharedConfig does not exist.
236        }
237        if (sharedConfig != null) {
238            file = sharedConfig;
239        } else if (!new File(getConfigFileName()).isAbsolute()) {
240            // must be relative, but we want it to
241            // be relative to the preferences directory
242            file = new File(FileUtil.getUserFilesPath() + getConfigFileName());
243        } else {
244            file = new File(getConfigFileName());
245        }
246        // don't try to load if doesn't exist, but mark as not OK
247        if (!file.exists()) {
248            preferenceFileExists = false;
249            configOK = false;
250            log.info("No pre-existing config file found, searched for '{}'", file.getPath());
251            return;
252        }
253        preferenceFileExists = true;
254
255        // ensure the UserPreferencesManager has loaded. Done on GUI
256        // thread as it can modify GUI objects
257        ThreadingUtil.runOnGUI(() -> {
258            InstanceManager.getDefault(jmri.UserPreferencesManager.class);
259        });
260
261        // now (attempt to) load the config file
262        try {
263            ConfigureManager cm = InstanceManager.getNullableDefault(jmri.ConfigureManager.class);
264            if (cm != null) {
265                configOK = cm.load(file);
266            } else {
267                configOK = false;
268            }
269            log.debug("end load config file {}, OK={}", file.getName(), configOK);
270        } catch (JmriException e) {
271            configOK = false;
272        }
273
274        if (sharedConfig != null) {
275            // sharedConfigs do not need deferred loads
276            configDeferredLoadOK = true;
277        } else if (SwingUtilities.isEventDispatchThread()) {
278            // To avoid possible locks, deferred load should be
279            // performed on the Swing thread
280            configDeferredLoadOK = doDeferredLoad(file);
281        } else {
282            try {
283                // Use invokeAndWait method as we don't want to
284                // return until deferred load is completed
285                SwingUtilities.invokeAndWait(() -> {
286                    configDeferredLoadOK = doDeferredLoad(file);
287                });
288            } catch (InterruptedException | InvocationTargetException ex) {
289                log.error("Exception creating system console frame:", ex);
290            }
291        }
292        if (sharedConfig == null && configOK == true && configDeferredLoadOK == true) {
293            log.info("Migrating preferences to new format...");
294            // migrate preferences
295            InstanceManager.getOptionalDefault(TabbedPreferences.class).ifPresent(tp -> {
296                //tp.init();
297                tp.saveContents();
298                InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent(cm -> {
299                    cm.storePrefs();
300                });
301                // notify user of change
302                log.info("Preferences have been migrated to new format.");
303                log.info("New preferences format will be used after JMRI is restarted.");
304            });
305        }
306    }
307
308    private boolean doDeferredLoad(File file) {
309        boolean result;
310        log.debug("start deferred load from config file {}", file.getName());
311        try {
312            ConfigureManager cm = InstanceManager.getNullableDefault(jmri.ConfigureManager.class);
313            if (cm != null) {
314                result = cm.loadDeferred(file);
315            } else {
316                log.error("Failed to get default configure manager");
317                result = false;
318            }
319        } catch (JmriException e) {
320            log.error("Unhandled problem loading deferred configuration:", e);
321            result = false;
322        }
323        log.debug("end deferred load from config file {}, OK={}", file.getName(), result);
324        return result;
325    }
326
327    /**
328     * Final actions before releasing control of the application to the user,
329     * invoked explicitly after object has been constructed in main().
330     */
331    protected void start() {
332        log.debug("main initialization done");
333    }
334
335    /**
336     * Set up the configuration file name at startup.
337     * <p>
338     * The Configuration File name variable holds the name used to load the
339     * configuration file during later startup processing. Applications invoke
340     * this method to handle the usual startup hierarchy:
341     * <ul>
342     * <li>If an absolute filename was provided on the command line, use it
343     * <li>If a filename was provided that's not absolute, consider it to be in
344     * the preferences directory
345     * <li>If no filename provided, use a default name (that's application specific)
346     * </ul>
347     * This name will be used for reading and writing the preferences. It need
348     * not exist when the program first starts up. This name may be proceeded
349     * with <em>config=</em>.
350     *
351     * @param def  Default value if no other is provided
352     * @param args Argument array from the main routine
353     */
354    static protected void setConfigFilename(String def, String[] args) {
355        // skip if org.jmri.Apps.configFilename is set
356        if (System.getProperty("org.jmri.Apps.configFilename") != null) {
357            return;
358        }
359        // save the configuration filename if present on the command line
360        if (args.length >= 1 && args[0] != null && !args[0].equals("") && !args[0].contains("=")) {
361            def = args[0];
362            log.debug("Config file was specified as: {}", args[0]);
363        }
364        for (String arg : args) {
365            String[] split = arg.split("=", 2);
366            if (split[0].equalsIgnoreCase("config")) {
367                def = split[1];
368                log.debug("Config file was specified as: {}", arg);
369            }
370        }
371        if (def != null) {
372            setJmriSystemProperty("configFilename", def);
373            log.debug("Config file set to: {}", def);
374        }
375    }
376
377    // We will use the value stored in the system property
378    static public String getConfigFileName() {
379        if (System.getProperty("org.jmri.Apps.configFilename") != null) {
380            return System.getProperty("org.jmri.Apps.configFilename");
381        }
382        return CONFIG_FILENAME;
383    }
384
385    static protected void setJmriSystemProperty(String key, String value) {
386        try {
387            String current = System.getProperty("org.jmri.Apps." + key);
388            if (current == null) {
389                System.setProperty("org.jmri.Apps." + key, value);
390            } else if (!current.equals(value)) {
391                log.warn("JMRI property {} already set to {}, skipping reset to {}", key, current, value);
392            }
393        } catch (Exception e) {
394            log.error("Unable to set JMRI property {} to {}due to exception", key, value, e);
395        }
396    }
397
398    /**
399     * The application decided to quit, handle that.
400     *
401     * @return always returns false
402     */
403    static public boolean handleQuit() {
404        log.debug("Start handleQuit");
405        try {
406            InstanceManager.getDefault(jmri.ShutDownManager.class).shutdown();
407        } catch (Exception e) {
408            log.error("Continuing after error in handleQuit", e);
409        }
410        return false;
411    }
412
413    /**
414     * The application decided to restart, handle that.
415     */
416    static public void handleRestart() {
417        log.debug("Start handleRestart");
418        try {
419            InstanceManager.getDefault(jmri.ShutDownManager.class).restart();
420        } catch (Exception e) {
421            log.error("Continuing after error in handleRestart", e);
422        }
423    }
424
425
426}