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}