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 // Load permission manager 138 InstanceManager.getDefault(PermissionManager.class); 139 // Needs to be declared final as we might need to 140 // refer to this on the Swing thread 141 File profileFile; 142 profileFilename = getConfigFileName().replaceFirst(".xml", ".properties"); 143 // decide whether name is absolute or relative 144 if (!new File(profileFilename).isAbsolute()) { 145 // must be relative, but we want it to 146 // be relative to the preferences directory 147 profileFile = new File(FileUtil.getPreferencesPath() + profileFilename); 148 } else { 149 profileFile = new File(profileFilename); 150 } 151 ProfileManager.getDefault().setConfigFile(profileFile); 152 // See if the profile to use has been specified on the command line as 153 // a system property org.jmri.profile as a profile id. 154 if (System.getProperties().containsKey(ProfileManager.SYSTEM_PROPERTY)) { 155 ProfileManager.getDefault().setActiveProfile(System.getProperty(ProfileManager.SYSTEM_PROPERTY)); 156 } 157 // @see jmri.profile.ProfileManager#migrateToProfiles Javadoc for conditions handled here 158 if (!profileFile.exists()) { // no profile config for this app 159 try { 160 if (ProfileManager.getDefault().migrateToProfiles(getConfigFileName())) { // migration or first use 161 // GUI should show message here 162 log.info("Migrated {}",Bundle.getMessage("ConfigMigratedToProfile")); 163 } 164 } catch (IOException | IllegalArgumentException ex) { 165 // GUI should show message here 166 log.error("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage()); 167 } 168 } 169 try { 170 // GUI should use ProfileManagerDialog.getStartingProfile here 171 if (ProfileManager.getStartingProfile() != null) { 172 // Manually setting the configFilename property since calling 173 // Apps.setConfigFilename() does not reset the system property 174 System.setProperty("org.jmri.Apps.configFilename", Profile.CONFIG_FILENAME); 175 Profile profile = ProfileManager.getDefault().getActiveProfile(); 176 if (profile != null) { 177 log.info("Starting with profile {}", profile.getId()); 178 } else { 179 log.info("Starting without a profile"); 180 } 181 } else { 182 log.error("Specify profile to use as command line argument."); 183 log.error("If starting with saved profile configuration, ensure the autoStart property is set to \"true\""); 184 log.error("Profiles not configurable. Using fallback per-application configuration."); 185 } 186 } catch (IOException ex) { 187 log.info("Profiles not configurable. Using fallback per-application configuration. Error: {}", ex.getMessage()); 188 } 189 } 190 191 protected void installConfigurationManager() { 192 // install a Preferences Action Factory 193 InstanceManager.store(new AppsPreferencesActionFactory(), JmriPreferencesActionFactory.class); 194 ConfigureManager cm = new AppsConfigurationManager(); 195 FileUtil.createDirectory(FileUtil.getUserFilesPath()); 196 InstanceManager.store(cm, ConfigureManager.class); 197 InstanceManager.setDefault(ConfigureManager.class, cm); 198 log.debug("config manager installed"); 199 } 200 201 protected void installManagers() { 202 // record startup 203 String appString = String.format("%s (v%s)", Application.getApplicationName(), Version.getCanonicalVersion()); 204 InstanceManager.getDefault(FileHistory.class).addOperation("app", appString, null); 205 206 // install the abstract action model that allows items to be added to the, both 207 // CreateButton and Perform Action Model use a common Abstract class 208 InstanceManager.store(new CreateButtonModel(), CreateButtonModel.class); 209 } 210 211 /** 212 * Invoked to load the preferences information, and in the process configure 213 * the system. The high-level steps are: 214 * <ul> 215 * <li>Locate the preferences file based through 216 * {@link FileUtil#getFile(String)} 217 * <li>See if the preferences file exists, and handle it if it doesn't 218 * <li>Obtain a {@link jmri.ConfigureManager} from the 219 * {@link jmri.InstanceManager} 220 * <li>Ask that ConfigureManager to load the file, in the process loading 221 * information into existing and new managers. 222 * <li>Do any deferred loads that are needed 223 * <li>If needed, migrate older formats 224 * </ul> 225 * (There's additional handling for shared configurations) 226 */ 227 protected void setAndLoadPreferenceFile() { 228 FileUtil.createDirectory(FileUtil.getUserFilesPath()); 229 final File file; 230 File sharedConfig = null; 231 try { 232 sharedConfig = FileUtil.getFile(FileUtil.PROFILE + Profile.SHARED_CONFIG); 233 if (!sharedConfig.canRead()) { 234 sharedConfig = null; 235 } 236 } catch (FileNotFoundException ex) { 237 // ignore - this only means that sharedConfig does not exist. 238 } 239 if (sharedConfig != null) { 240 file = sharedConfig; 241 } else if (!new File(getConfigFileName()).isAbsolute()) { 242 // must be relative, but we want it to 243 // be relative to the preferences directory 244 file = new File(FileUtil.getUserFilesPath() + getConfigFileName()); 245 } else { 246 file = new File(getConfigFileName()); 247 } 248 // don't try to load if doesn't exist, but mark as not OK 249 if (!file.exists()) { 250 preferenceFileExists = false; 251 configOK = false; 252 log.info("No pre-existing config file found, searched for '{}'", file.getPath()); 253 return; 254 } 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 428}