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}