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}