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 final static 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    static public 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                // Manually setting the configFilename property since calling
169                // Apps.setConfigFilename() does not reset the system property
170                System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME);
171                Profile profile = ProfileManager.getDefault().getActiveProfile();
172                if (profile != null) {
173                    log.info("Starting with profile {}", profile.getId());
174                } else {
175                    log.info("Starting without a profile");
176                }
177            } else {
178                log.error("Specify profile to use as command line argument.");
179                log.error("If starting with saved profile configuration, ensure the autoStart property is set to \"true\"");
180                log.error("Profiles not configurable. Using fallback per-application configuration.");
181            }
182        } catch (IOException ex) {
183            log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage());
184        }
185    }
186
187    protected void installConfigurationManager() {
188        // install a Preferences Action Factory
189        InstanceManager.store(new AppsPreferencesActionFactory(), JmriPreferencesActionFactory.class);
190        ConfigureManager cm = new AppsConfigurationManager();
191        FileUtil.createDirectory(FileUtil.getUserFilesPath());
192        InstanceManager.store(cm, ConfigureManager.class);
193        InstanceManager.setDefault(ConfigureManager.class, cm);
194        log.debug("config manager installed");
195    }
196
197    protected void installManagers() {
198        // record startup
199        String appString = String.format("%s (v%s)", Application.getApplicationName(), Version.getCanonicalVersion());
200        InstanceManager.getDefault(FileHistory.class).addOperation("app", appString, null);
201
202        // install the abstract action model that allows items to be added to the, both
203        // CreateButton and Perform Action Model use a common Abstract class
204        InstanceManager.store(new CreateButtonModel(), CreateButtonModel.class);
205    }
206
207    /**
208     * Invoked to load the preferences information, and in the process configure
209     * the system. The high-level steps are:
210     * <ul>
211     * <li>Locate the preferences file based through
212     * {@link FileUtil#getFile(String)}
213     * <li>See if the preferences file exists, and handle it if it doesn't
214     * <li>Obtain a {@link jmri.ConfigureManager} from the
215     * {@link jmri.InstanceManager}
216     * <li>Ask that ConfigureManager to load the file, in the process loading
217     * information into existing and new managers.
218     * <li>Do any deferred loads that are needed
219     * <li>If needed, migrate older formats
220     * </ul>
221     * (There's additional handling for shared configurations)
222     */
223    protected void setAndLoadPreferenceFile() {
224        FileUtil.createDirectory(FileUtil.getUserFilesPath());
225        final File file;
226        File sharedConfig = null;
227        try {
228            sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG);
229            if (!sharedConfig.canRead()) {
230                sharedConfig = null;
231            }
232        } catch (FileNotFoundException ex) {
233            // ignore - this only means that sharedConfig does not exist.
234        }
235        if (sharedConfig != null) {
236            file = sharedConfig;
237            log.trace("Try preferences from sharedConfig {}", file.getPath());
238        } else if (!new File(getConfigFileName()).isAbsolute()) {
239            // must be relative, but we want it to
240            // be relative to the preferences directory
241            file = new File(FileUtil.getUserFilesPath() + getConfigFileName());
242            log.trace("Try references from getUserFilesPath {}", file.getPath());
243        } else {
244            file = new File(getConfigFileName());
245            log.trace("Try references from getConfigFileName {}", file.getPath());
246        }
247        // don't try to load if doesn't exist, but mark as not OK
248        if (!file.exists()) {
249            preferenceFileExists = false;
250            configOK = false;
251            log.info("No pre-existing config file found, searched for '{}'", file.getPath());
252            return;
253        }
254        log.debug("Found preferences file '{}'", file.getPath());
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    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AppsBase.class);
428}