001package jmri.managers; 002 003import java.beans.PropertyChangeEvent; 004import java.beans.PropertyChangeListener; 005import java.util.ArrayList; 006import java.util.HashMap; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Locale; 010import java.util.Map; 011import java.util.Set; 012import java.util.prefs.BackingStoreException; 013import java.util.prefs.Preferences; 014import javax.annotation.CheckForNull; 015import javax.annotation.Nonnull; 016import jmri.AddressedProgrammerManager; 017import jmri.CommandStation; 018import jmri.ConfigureManager; 019import jmri.ConsistManager; 020import jmri.GlobalProgrammerManager; 021import jmri.InstanceManager; 022import jmri.PowerManager; 023import jmri.ThrottleManager; 024import jmri.SystemConnectionMemo; 025import jmri.jmrix.SystemConnectionMemoManager; 026import jmri.jmrix.internal.InternalSystemConnectionMemo; 027import jmri.profile.Profile; 028import jmri.profile.ProfileUtils; 029import jmri.spi.PreferencesManager; 030import jmri.util.prefs.AbstractPreferencesManager; 031import jmri.util.prefs.InitializationException; 032import org.openide.util.lookup.ServiceProvider; 033 034/** 035 * Records and executes a desired set of defaults for the JMRI InstanceManager 036 * and ProxyManagers. 037 * <p> 038 * Provided that a connection provides a default, this verifies, unless the 039 * per-profile property {@code jmri-managers.allInternalDefaults} is 040 * {@code true}, that a non-Internal connection (other than type None in the 041 * preferences window) is the default for at least one type of manager. 042 * <p> 043 * allInternalDefaults is preserved as a preference when set here, but 044 * {@link #setAllInternalDefaultsValid} is not (originally) invoked from the 045 * GUI. 046 * 047 * @author Bob Jacobsen Copyright (C) 2010 048 * @author Randall Wood Copyright (C) 2015, 2017 049 * @since 2.9.4 050 * @see jmri.SystemConnectionMemo#provides(java.lang.Class) 051 */ 052@ServiceProvider(service = PreferencesManager.class) 053public class ManagerDefaultSelector extends AbstractPreferencesManager { 054 055 public final HashMap<Class<?>, String> defaults = new HashMap<>(); 056 private PropertyChangeListener memoListener; 057 private boolean allInternalDefaultsValid = false; 058 public final static String ALL_INTERNAL_DEFAULTS = "allInternalDefaults"; 059 060 public ManagerDefaultSelector() { 061 memoListener = (PropertyChangeEvent e) -> { 062 log.trace("memoListener fired via {}", e); 063 switch (e.getPropertyName()) { 064 case SystemConnectionMemo.USER_NAME: 065 String oldName = (String) e.getOldValue(); 066 String newName = (String) e.getNewValue(); 067 log.debug("ConnectionNameChanged from \"{}\" to \"{}\"", oldName, newName); 068 // Takes a copy of the keys to avoid ConcurrentModificationException. 069 new HashSet<>(defaults.keySet()).forEach((c) -> { 070 String connectionName = this.defaults.get(c); 071 if (connectionName.equals(oldName)) { 072 ManagerDefaultSelector.this.defaults.put(c, newName); 073 } 074 }); 075 this.firePropertyChange("Updated", null, null); 076 break; 077 case SystemConnectionMemo.DISABLED: 078 Boolean newState = (Boolean) e.getNewValue(); 079 if (newState) { 080 String disabledName = ((SystemConnectionMemo) e.getSource()).getUserName(); 081 log.debug("ConnectionDisabled true: \"{}\"", disabledName); 082 removeConnectionAsDefault(disabledName); 083 } 084 break; 085 default: 086 log.debug("ignoring notification of \"{}\"", e.getPropertyName()); 087 break; 088 } 089 }; 090 SystemConnectionMemoManager.getDefault().addPropertyChangeListener((PropertyChangeEvent e) -> { 091 // 092 // Note that when JMRI is starting, this listener does 093 // trigger as connections are added, but that after the 094 // configured connections are set, the defaults are reset 095 // when configure(Profile) is called. We do, however, 096 // want these to be set immediately when a new profile 097 // is launched for the first time, so these listeners 098 // need to be in place as early as possible. 099 // 100 log.trace("addPropertyChangeListener fired via {}", e); 101 switch (e.getPropertyName()) { 102 case SystemConnectionMemoManager.CONNECTION_REMOVED: 103 if (e.getOldValue() instanceof SystemConnectionMemo) { 104 SystemConnectionMemo memo = (SystemConnectionMemo) e.getOldValue(); 105 String removedName = ((SystemConnectionMemo) e.getOldValue()).getUserName(); 106 log.debug("ConnectionRemoved for \"{}\"", removedName); 107 removeConnectionAsDefault(removedName); 108 memo.removePropertyChangeListener(this.memoListener); 109 } 110 break; 111 case SystemConnectionMemoManager.CONNECTION_ADDED: 112 if (e.getNewValue() instanceof SystemConnectionMemo) { 113 SystemConnectionMemo memo = (SystemConnectionMemo) e.getNewValue(); 114 memo.addPropertyChangeListener(this.memoListener); 115 // check for special case of anything else then Internal 116 // and set first system to be default for all provided defaults 117 List<SystemConnectionMemo> list = InstanceManager.getList(SystemConnectionMemo.class); 118 119 if (log.isDebugEnabled()) { 120 log.debug("Start CONNECTION_ADDED processing with {} existing", list.size()); 121 for (int i = 0; i < list.size(); i++) { 122 log.debug(" System {}: {} (\"{}\")", i, list.get(i), list.get(i).getUserName()); 123 } 124 } 125 126 if ((list.size() == 1 && !(list.get(0) instanceof InternalSystemConnectionMemo)) || 127 (list.size() == 2 && !(list.get(0) instanceof InternalSystemConnectionMemo) && list.get(1) instanceof InternalSystemConnectionMemo)) { 128 // first system added is hardware, gets defaults for everything it supports 129 log.debug("First real system added, reset defaults"); 130 for (Item item : knownManagers) { 131 if (memo.provides(item.managerClass)) { 132 this.setDefault(item.managerClass, memo.getUserName()); 133 } 134 } 135 } 136 // any new connection that provides a missing default 137 // gets set as the default for that missing default 138 // use new HashSet over this.defaults.keySet to avoid 139 // ConcurrentModificationException on this.defaults 140 new HashSet<>(defaults.keySet()).forEach((cls) -> { 141 String userName = defaults.get(cls); 142 if (userName == null && memo.provides(cls)) { 143 this.setDefault(cls, memo.getUserName()); 144 } 145 }); 146 } 147 break; 148 default: 149 log.debug("ignoring notification of \"{}\"", e.getPropertyName()); 150 break; 151 } 152 }); 153 InstanceManager.getList(SystemConnectionMemo.class).forEach((memo) -> { 154 memo.addPropertyChangeListener(this.memoListener); 155 }); 156 } 157 158 // remove connection's record 159 void removeConnectionAsDefault(String removedName) { 160 ArrayList<Class<?>> tmpArray = new ArrayList<>(); 161 defaults.keySet().stream().forEach((c) -> { 162 String connectionName = ManagerDefaultSelector.this.defaults.get(c); 163 if (connectionName.equals(removedName)) { 164 log.debug("Connection {} has been removed as the default for {}", removedName, c); 165 tmpArray.add(c); 166 } 167 }); 168 tmpArray.stream().forEach((tmpArray1) -> { 169 ManagerDefaultSelector.this.defaults.remove(tmpArray1); 170 }); 171 this.firePropertyChange("Updated", null, null); 172 } 173 174 /** 175 * Return the userName of the system that provides the default instance for 176 * a specific class. 177 * 178 * @param managerClass the specific type, for example, TurnoutManager, for 179 * which a default system is desired 180 * @return userName of the system, or null if none set 181 */ 182 public String getDefault(Class<?> managerClass) { 183 return defaults.get(managerClass); 184 } 185 186 /** 187 * Record the userName of the system that provides the default instance for 188 * a specific class. 189 * <p> 190 * To ensure compatibility of different preference versions, only classes 191 * that are current registered are preserved. This way, reading in an old 192 * file will just have irrelevant items ignored. 193 * 194 * @param managerClass the specific type, for example, TurnoutManager, for 195 * which a default system is desired 196 * @param userName of the system, or null if none set 197 */ 198 public void setDefault(Class<?> managerClass, String userName) { 199 for (Item item : knownManagers) { 200 if (item.managerClass.equals(managerClass)) { 201 log.debug(" setting default for \"{}\" to \"{}\" by request", managerClass, userName); 202 defaults.put(managerClass, userName); 203 return; 204 } 205 } 206 log.warn("Ignoring preference for class {} with name {}", managerClass, userName); 207 } 208 209 /** 210 * Load into InstanceManager 211 * 212 * @param profile the profile to configure against 213 * @return an exception that can be passed to the user or null if no errors 214 * occur 215 */ 216 @CheckForNull 217 @SuppressWarnings({"unchecked", "rawtypes"}) 218 public InitializationException configure(Profile profile) { 219 InitializationException error = null; 220 List<SystemConnectionMemo> connList = InstanceManager.getList(SystemConnectionMemo.class); 221 log.debug("configure defaults into InstanceManager from {} memos, {} defaults", connList.size(), defaults.keySet().size()); 222 // Takes a copy to avoid ConcurrentModificationException. 223 Set<Class<?>> keys = new HashSet<>(defaults.keySet()); 224 for (Class<?> c : keys) { 225 // 'c' is the class to load 226 String connectionName = defaults.get(c); 227 // have to find object of that type from proper connection 228 boolean found = false; 229 for (SystemConnectionMemo memo : connList) { 230 String testName = memo.getUserName(); 231 if (testName.equals(connectionName)) { 232 found = true; 233 // match, store 234 try { 235 if (memo.provides(c)) { 236 log.debug(" setting default for \"{}\" to \"{}\" in configure", c, memo.get(c)); 237 InstanceManager.setDefault((Class)c, (Object)memo.get(c)); // Java generics doesn't work in this case to type cast to (Class) and (Object) 238 } 239 } catch (NullPointerException ex) { 240 String englishMsg = Bundle.getMessage(Locale.ENGLISH, "ErrorNullDefault", memo.getUserName(), c); // NOI18N 241 String localizedMsg = Bundle.getMessage("ErrorNullDefault", memo.getUserName(), c); // NOI18N 242 error = new InitializationException(englishMsg, localizedMsg); 243 log.warn("SystemConnectionMemo for {} ({}) provides a null {} instance", memo.getUserName(), memo.getClass(), c); 244 } 245 break; 246 } else { 247 log.debug(" memo name didn't match: {} vs {}", testName, connectionName); 248 } 249 } 250 /* 251 * If the set connection can not be found then we shall set the manager default to use what 252 * has currently been set. 253 */ 254 if (!found) { 255 log.debug("!found, so resetting"); 256 String currentName = null; 257 if (c == ThrottleManager.class && InstanceManager.getOptionalDefault(ThrottleManager.class).isPresent()) { 258 currentName = InstanceManager.throttleManagerInstance().getUserName(); 259 } else if (c == PowerManager.class && InstanceManager.getOptionalDefault(PowerManager.class).isPresent()) { 260 currentName = InstanceManager.getDefault(PowerManager.class).getUserName(); 261 } 262 if (currentName != null) { 263 log.warn("The configured {} for {} can not be found so will use the default {}", connectionName, c, currentName); 264 this.defaults.put(c, currentName); 265 } 266 } 267 } 268 if (!isPreferencesValid(profile, connList)) { 269 error = new InitializationException(Bundle.getMessage(Locale.ENGLISH, "ManagerDefaultSelector.AllInternal"), Bundle.getMessage("ManagerDefaultSelector.AllInternal")); 270 } 271 return error; 272 } 273 274 // Define set of items that we remember defaults for, manually maintained because 275 // there are lots of JMRI-internal types of no interest to the user and/or not system-specific. 276 // This grows if you add something to the SystemConnectionMemo system 277 final public Item[] knownManagers = new Item[]{ 278 new Item("<html>Throttles</html>", ThrottleManager.class), 279 new Item("<html>Power<br>Control</html>", PowerManager.class), 280 new Item("<html>Command<br>Station</html>", CommandStation.class), 281 new Item("<html>Service<br>Programmer</html>", GlobalProgrammerManager.class), 282 new Item("<html>Ops Mode<br>Programmer</html>", AddressedProgrammerManager.class), 283 new Item("<html>Consists</html>", ConsistManager.class) 284 }; 285 286 @Override 287 public void initialize(Profile profile) throws InitializationException { 288 if (!this.isInitialized(profile)) { 289 Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true); // NOI18N 290 Preferences defaultsPreferences = preferences.node("defaults"); 291 try { 292 for (String name : defaultsPreferences.keys()) { 293 String connection = defaultsPreferences.get(name, null); 294 Class<?> cls = this.classForName(name); 295 log.debug("Loading default {} for {}", connection, name); 296 if (cls != null) { 297 this.defaults.put(cls, connection); 298 log.debug("Loaded default {} for {}", connection, cls); 299 } 300 } 301 this.allInternalDefaultsValid = preferences.getBoolean(ALL_INTERNAL_DEFAULTS, this.allInternalDefaultsValid); 302 } catch (BackingStoreException ex) { 303 log.info("Unable to read preferences for Default Selector."); 304 } 305 InitializationException ex = this.configure(profile); 306 InstanceManager.getOptionalDefault(ConfigureManager.class).ifPresent((manager) -> { 307 manager.registerPref(this); // allow profile configuration to be written correctly 308 }); 309 this.setInitialized(profile, true); 310 if (ex != null) { 311 this.addInitializationException(profile, ex); 312 throw ex; 313 } 314 } 315 } 316 317 @Override 318 public void savePreferences(Profile profile) { 319 Preferences preferences = ProfileUtils.getPreferences(profile, this.getClass(), true); // NOI18N 320 Preferences defaultsPreferences = preferences.node("defaults"); 321 try { 322 this.defaults.keySet().stream().forEach((cls) -> { 323 defaultsPreferences.put(this.nameForClass(cls), this.defaults.get(cls)); 324 }); 325 preferences.putBoolean(ALL_INTERNAL_DEFAULTS, this.allInternalDefaultsValid); 326 preferences.sync(); 327 } catch (BackingStoreException ex) { 328 log.error("Unable to save preferences for Default Selector.", ex); 329 } 330 } 331 332 private boolean isPreferencesValid(Profile profile, List<SystemConnectionMemo> connections) { 333 log.trace("isPreferencesValid start"); 334 if (allInternalDefaultsValid) { 335 log.trace("allInternalDefaultsValid returns true"); 336 return true; 337 } 338 boolean usesExternalConnections = false; 339 340 // classes of managers being provided, and set of which SystemConnectionMemos can provide each 341 Map<Class<?>, Set<SystemConnectionMemo>> providing = new HashMap<>(); 342 343 // list of all external providers (i.e. SystemConnectionMemos) that provide at least one known manager type 344 Set<SystemConnectionMemo> providers = new HashSet<>(); 345 346 if (connections.size() > 1) { 347 connections.stream().filter((memo) -> (!(memo instanceof InternalSystemConnectionMemo))).forEachOrdered((memo) -> { 348 // populate providers by adding all external (non-internal) connections that provide at least one default 349 for (Item item : knownManagers) { 350 if (memo.provides(item.managerClass)) { 351 providers.add(memo); 352 break; 353 } 354 } 355 }); 356 // if there are no external providers of managers, no further checks are needed 357 if (providers.size() >= 1) { 358 // build a list of defaults provided by external connections 359 providers.stream().forEach((memo) -> { 360 for (Item item : knownManagers) { 361 if (memo.provides(item.managerClass)) { 362 Set<SystemConnectionMemo> provides = providing.getOrDefault(item.managerClass, new HashSet<>()); 363 provides.add(memo); 364 providing.put(item.managerClass, provides); 365 } 366 } 367 }); 368 369 if (log.isDebugEnabled()) { 370 // avoid unneeded overhead of looping through providers 371 providing.forEach((cls, clsProviders) -> { 372 log.debug("{} default provider is {}, is provided by:", cls.getName(), defaults.get(cls)); 373 clsProviders.forEach((provider) -> { 374 log.debug(" user name: {}", provider.getUserName()); 375 }); 376 }); 377 } 378 379 for (SystemConnectionMemo memo : providers) { 380 if (providing.keySet().stream().filter((cls) -> { 381 Set<SystemConnectionMemo> provides = providing.get(cls); 382 log.debug("{} is provided by {} out of {} connections", cls.getName(), provides.size(), providers.size()); 383 log.trace("memo stream returns {} due to producers.size() {}", (provides.size() > 0), provides.size()); 384 return (provides.size() > 0); 385 }).anyMatch((cls) -> { 386 log.debug("{} has an external default", cls); 387 if (defaults.get(cls) == null) { 388 log.trace("memo stream returns true because there's no default defined and an external provider exists"); 389 return true; 390 } 391 log.trace("memo stream returns {} due to memo.getUserName() {} and {}", (memo.getUserName().equals(defaults.get(cls))), memo.getUserName(), defaults.get(cls)); 392 return memo.getUserName().equals(defaults.get(cls)); 393 })) { 394 log.trace("setting usesExternalConnections true"); 395 usesExternalConnections = true; 396 // no need to check further 397 break; 398 } 399 } 400 } 401 } 402 log.trace("method end returns {} due to providers.size() {} and usesExternalConnections {}", (providers.size() >= 1 ? usesExternalConnections : true), providers.size(), usesExternalConnections); 403 return providers.size() >= 1 ? usesExternalConnections : true; 404 } 405 406 public boolean isPreferencesValid(Profile profile) { 407 return isPreferencesValid(profile, InstanceManager.getList(SystemConnectionMemo.class)); 408 } 409 410 public static class Item { 411 412 public String typeName; 413 public Class<?> managerClass; 414 415 Item(String typeName, Class<?> managerClass) { 416 this.typeName = typeName; 417 this.managerClass = managerClass; 418 } 419 } 420 421 private String nameForClass(@Nonnull Class<?> cls) { 422 return cls.getCanonicalName().replace('.', '-'); 423 } 424 425 private Class<?> classForName(@Nonnull String name) { 426 try { 427 return Class.forName(name.replace('-', '.')); 428 } catch (ClassNotFoundException ex) { 429 log.error("Could not find class for {}", name); 430 return null; 431 } 432 } 433 434 /** 435 * Check if having all defaults assigned to internal connections should be 436 * considered is valid in the presence of an external System Connection. 437 * 438 * @return true if having all internal defaults should be valid; false 439 * otherwise 440 */ 441 public boolean isAllInternalDefaultsValid() { 442 return allInternalDefaultsValid; 443 } 444 445 /** 446 * Set if having all defaults assigned to internal connections should be 447 * considered is valid in the presence of an external System Connection. 448 * 449 * @param isAllInternalDefaultsValid true if having all internal defaults 450 * should be valid; false otherwise 451 */ 452 public void setAllInternalDefaultsValid(boolean isAllInternalDefaultsValid) { 453 this.allInternalDefaultsValid = isAllInternalDefaultsValid; 454 } 455 456 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ManagerDefaultSelector.class); 457}