001package jmri.jmrix; 002 003import java.util.*; 004 005import javax.annotation.Nonnull; 006import javax.annotation.OverridingMethodsMustInvokeSuper; 007 008import jmri.*; 009import jmri.SystemConnectionMemo; 010import jmri.beans.Bean; 011import jmri.implementation.DccConsistManager; 012import jmri.implementation.NmraConsistManager; 013import jmri.util.NamedBeanComparator; 014 015import jmri.util.startup.StartupActionFactory; 016 017/** 018 * Lightweight abstract class to denote that a system is active, and provide 019 * general information. 020 * <p> 021 * Objects of specific subtypes of this are registered in the 022 * {@link InstanceManager} to activate their particular system. 023 * 024 * @author Bob Jacobsen Copyright (C) 2010 025 */ 026public abstract class DefaultSystemConnectionMemo extends Bean implements SystemConnectionMemo, Disposable { 027 028 private boolean disabled = false; 029 private Boolean disabledAsLoaded = null; // Boolean can be true, false, or null 030 private String prefix; 031 private String prefixAsLoaded; 032 private String userName; 033 private String userNameAsLoaded; 034 protected Map<Class<?>,Object> classObjectMap; 035 036 protected DefaultSystemConnectionMemo(@Nonnull String prefix, @Nonnull String userName) { 037 classObjectMap = new HashMap<>(); 038 if (this instanceof CaptiveSystemConnectionMemo) { 039 this.prefix = prefix; 040 this.userName = userName; 041 return; 042 } 043 log.debug("SystemConnectionMemo created for prefix \"{}\" user name \"{}\"", prefix, userName); 044 if (!setSystemPrefix(prefix)) { 045 int x = 2; 046 while (!setSystemPrefix(prefix + x)) { 047 x++; 048 } 049 log.debug("created system prefix {}{}", prefix, x); 050 } 051 052 if (!setUserName(userName)) { 053 int x = 2; 054 while (!setUserName(userName + x)) { 055 x++; 056 } 057 log.debug("created user name {}{}", prefix, x); 058 } 059 // reset to null so these get set by the first setPrefix/setUserName 060 // call after construction 061 this.prefixAsLoaded = null; 062 this.userNameAsLoaded = null; 063 } 064 065 /** 066 * Register with the SystemConnectionMemoManager and InstanceManager with proper 067 * ID for later retrieval as a generic system. 068 * <p> 069 * This operation should occur only when the SystemConnectionMemo is ready for use. 070 */ 071 @Override 072 public void register() { 073 log.debug("register as SystemConnectionMemo, really of type {}", this.getClass()); 074 SystemConnectionMemoManager.getDefault().register(this); 075 } 076 077 /** 078 * Provide access to the system prefix string. 079 * <p> 080 * This was previously called the "System letter". 081 * 082 * @return System prefix 083 */ 084 @Override 085 public String getSystemPrefix() { 086 return prefix; 087 } 088 089 /** 090 * Set the system prefix. 091 * 092 * @param systemPrefix prefix to use for this system connection 093 * @throws java.lang.NullPointerException if systemPrefix is null 094 * @return true if the system prefix could be set 095 */ 096 @Override 097 public final boolean setSystemPrefix(@Nonnull String systemPrefix) { 098 Objects.requireNonNull(systemPrefix); 099 // return true if systemPrefix is not being changed 100 if (systemPrefix.equals(prefix)) { 101 if (this.prefixAsLoaded == null) { 102 this.prefixAsLoaded = systemPrefix; 103 } 104 return true; 105 } 106 String oldPrefix = prefix; 107 if (SystemConnectionMemoManager.getDefault().isSystemPrefixAvailable(systemPrefix)) { 108 prefix = systemPrefix; 109 if (this.prefixAsLoaded == null) { 110 this.prefixAsLoaded = systemPrefix; 111 } 112 this.propertyChangeSupport.firePropertyChange(SYSTEM_PREFIX, oldPrefix, systemPrefix); 113 return true; 114 } 115 log.debug("setSystemPrefix false for \"{}\"", systemPrefix); 116 return false; 117 } 118 119 /** 120 * Provide access to the system user name string. 121 * <p> 122 * This was previously fixed at configuration time. 123 * 124 * @return User name of the connection 125 */ 126 @Override 127 public String getUserName() { 128 return userName; 129 } 130 131 /** 132 * Set the user name for the system connection. 133 * 134 * @param userName user name to use for this system connection 135 * @throws java.lang.NullPointerException if name is null 136 * @return true if the user name could be set. 137 */ 138 @Override 139 public final boolean setUserName(@Nonnull String userName) { 140 Objects.requireNonNull(userName); 141 if (userName.equals(this.userName)) { 142 if (this.userNameAsLoaded == null) { 143 this.userNameAsLoaded = userName; 144 } 145 return true; 146 } 147 String oldUserName = this.userName; 148 if (SystemConnectionMemoManager.getDefault().isUserNameAvailable(userName)) { 149 this.userName = userName; 150 if (this.userNameAsLoaded == null) { 151 this.userNameAsLoaded = userName; 152 } 153 this.propertyChangeSupport.firePropertyChange(USER_NAME, oldUserName, userName); 154 return true; 155 } 156 return false; 157 } 158 159 /** 160 * Check if this connection provides a specific manager type. This method 161 * <strong>must</strong> return false if a manager for the specific type is 162 * not provided, and <strong>must</strong> return true if a manager for the 163 * specific type is provided. 164 * 165 * @param c The class type for the manager to be provided 166 * @return true if the specified manager is provided 167 * @see #get(java.lang.Class) 168 */ 169 @OverridingMethodsMustInvokeSuper 170 @Override 171 public boolean provides(Class<?> c) { 172 if (disabled) { 173 return false; 174 } 175 if (c.equals(jmri.ConsistManager.class)) { 176 return classObjectMap.get(c) != null || provides(CommandStation.class) || provides(AddressedProgrammerManager.class); 177 } else { 178 return classObjectMap.containsKey(c); 179 } 180 } 181 182 /** 183 * Get a manager for a specific type. This method <strong>must</strong> 184 * return a non-null value if {@link #provides(java.lang.Class)} is true for 185 * the type, and <strong>must</strong> return null if provides() is false 186 * for the type. 187 * 188 * @param <T> Type of manager to get 189 * @param type Type of manager to get 190 * @return The manager or null if provides() is false for T 191 * @see #provides(java.lang.Class) 192 */ 193 @OverridingMethodsMustInvokeSuper 194 @SuppressWarnings("unchecked") // dynamic checking done on cast of getConsistManager 195 @Override 196 public <T> T get(Class<T> type) { 197 if (disabled) { 198 return null; 199 } 200 if (type.equals(ConsistManager.class)) { 201 return (T) getConsistManager(); 202 } else { 203 return (T) classObjectMap.get(type); // nothing, by default 204 } 205 } 206 207 /** 208 * Dispose of System Connection. 209 * <p> 210 * Removes objects from classObjectMap after 211 * calling dispose if Disposable. 212 * Removes these objects from InstanceManager. 213 */ 214 @Override 215 public void dispose() { 216 Set<Class<?>> keySet = new HashSet<>(classObjectMap.keySet()); 217 keySet.forEach(this::removeRegisteredObject); 218 SystemConnectionMemoManager.getDefault().deregister(this); 219 } 220 221 /** 222 * Remove single class object. 223 * Removes from InstanceManager 224 * Removes from Memo class list 225 * Call object dispose if class implements Disposable 226 * @param <T> class Type 227 * @param c actual class 228 */ 229 private <T> void removeRegisteredObject(Class<T> c) { 230 T object = get(c); 231 if (object != null) { 232 InstanceManager.deregister(object, c); 233 deregister(object, c); 234 disposeIfPossible(c, object); 235 } 236 } 237 238 private <T> void disposeIfPossible(Class<T> c, T object) { 239 if(object instanceof Disposable) { 240 try { 241 ((Disposable)object).dispose(); 242 } catch (Exception e) { 243 log.warn("Exception while disposing object of type {} in memo of type {}.", c.getName(), this.getClass().getName(), e); 244 } 245 } 246 } 247 248 /** 249 * Get if the System Connection is currently Disabled. 250 * 251 * @return true if Disabled, else false. 252 */ 253 @Override 254 public boolean getDisabled() { 255 return disabled; 256 } 257 258 /** 259 * Set if the System Connection is currently Disabled. 260 * <p> 261 * disabledAsLoaded is only set once. 262 * Sends PropertyChange on change of disabled status. 263 * 264 * @param disabled true to disable, false to enable. 265 */ 266 @Override 267 public void setDisabled(boolean disabled) { 268 if (this.disabledAsLoaded == null) { 269 // only set first time 270 this.disabledAsLoaded = disabled; 271 } 272 if (disabled != this.disabled) { 273 boolean oldDisabled = this.disabled; 274 this.disabled = disabled; 275 this.propertyChangeSupport.firePropertyChange(DISABLED, oldDisabled, disabled); 276 } 277 } 278 279 /** 280 * Get the Comparator to be used for two NamedBeans. This is typically an 281 * {@link NamedBeanComparator}, but may be any Comparator that works for 282 * this connection type. 283 * 284 * @param <B> the type of NamedBean 285 * @param type the class of NamedBean 286 * @return the Comparator 287 */ 288 @Override 289 public abstract <B extends NamedBean> Comparator<B> getNamedBeanComparator(Class<B> type); 290 291 /** 292 * Provide a factory for getting startup actions. 293 * <p> 294 * This is a bound, read-only, property under the name "actionFactory". 295 * 296 * @return the factory 297 */ 298 @Nonnull 299 @Override 300 public StartupActionFactory getActionFactory() { 301 return new ResourceBundleStartupActionFactory(getActionModelResourceBundle()); 302 } 303 304 protected abstract ResourceBundle getActionModelResourceBundle(); 305 306 /** 307 * Get if connection is dirty. 308 * Checked fields are disabled, prefix, userName 309 * 310 * @return true if changed since loaded 311 */ 312 @Override 313 public boolean isDirty() { 314 return ((this.disabledAsLoaded == null || this.disabledAsLoaded != this.disabled) 315 || (this.prefixAsLoaded == null || !this.prefixAsLoaded.equals(this.prefix)) 316 || (this.userNameAsLoaded == null || !this.userNameAsLoaded.equals(this.userName))); 317 } 318 319 @Override 320 public boolean isRestartRequired() { 321 return this.isDirty(); 322 } 323 324 /** 325 * Provide access to the ConsistManager for this particular connection. 326 * 327 * @return the provided ConsistManager or null if the connection does not 328 * provide a ConsistManager 329 */ 330 public ConsistManager getConsistManager() { 331 return (ConsistManager) classObjectMap.computeIfAbsent(ConsistManager.class,(Class<?> c) -> { return generateDefaultConsistManagerForConnection(); }); 332 } 333 334 private ConsistManager generateDefaultConsistManagerForConnection(){ 335 if (provides(jmri.CommandStation.class)) { 336 return new NmraConsistManager(get(jmri.CommandStation.class)); 337 } else if (provides(jmri.AddressedProgrammerManager.class)) { 338 return new DccConsistManager(get(jmri.AddressedProgrammerManager.class)); 339 } 340 return null; 341 } 342 343 public void setConsistManager(ConsistManager c) { 344 store(c, ConsistManager.class); 345 jmri.InstanceManager.store(c, ConsistManager.class); 346 } 347 348 /** 349 * Store a class object to the system connection memo. 350 * <p> 351 * Does NOT register the class with InstanceManager. 352 * <p> 353 * On memo dispose, each class will be removed from InstanceManager, 354 * and if the class implements disposable, the dispose method is called. 355 * @param <T> Class type obtained from item object. 356 * @param item the class object to store, eg. mySensorManager 357 * @param type Class type, eg. SensorManager.class 358 */ 359 public <T> void store(@Nonnull T item, @Nonnull Class<T> type){ 360 Map<Class<?>,Object> classObjectMapCopy = classObjectMap; 361 classObjectMap.put(type,item); 362 if ( !classObjectMapCopy.containsValue(item) ) { 363 propertyChangeSupport.firePropertyChange(STORE, null, item); 364 } 365 } 366 367 /** 368 * Remove a class object from the system connection memo classObjectMap. 369 * <p> 370 * Does NOT remove the class from InstanceManager. 371 * 372 * @param <T> Class type obtained from item object. 373 * @param item the class object to store, eg. mySensorManager 374 * @param type Class type, eg. SensorManager.class 375 */ 376 public <T> void deregister(@Nonnull T item, @Nonnull Class<T> type){ 377 Map<Class<?>,Object> classObjectMapCopy = classObjectMap; 378 classObjectMap.remove(type,item); 379 if ( classObjectMapCopy.containsValue(item) ) { 380 propertyChangeSupport.firePropertyChange(DEREGISTER, item, null); 381 } 382 } 383 384 /** 385 * Duration in milliseconds of interval between separate Turnout commands on the same connection. 386 * <p> 387 * Change from e.g. connection config dialog and scripts using {@link #setOutputInterval(int)} 388 */ 389 private int _interval = getDefaultOutputInterval(); 390 391 /** 392 * Default interval 250ms. 393 * {@inheritDoc} 394 */ 395 @Override 396 public int getDefaultOutputInterval(){ 397 return 250; 398 } 399 400 /** 401 * Get the connection specific OutputInterval (in ms) to wait between/before commands 402 * are sent, configured in AdapterConfig. 403 * Used in {@link jmri.implementation.AbstractTurnout#setCommandedStateAtInterval(int)}. 404 */ 405 @Override 406 public int getOutputInterval() { 407 log.debug("Getting interval {}", _interval); 408 return _interval; 409 } 410 411 @Override 412 public void setOutputInterval(int newInterval) { 413 log.debug("Setting interval from {} to {}", _interval, newInterval); 414 this.propertyChangeSupport.firePropertyChange(INTERVAL, _interval, newInterval); 415 _interval = newInterval; 416 } 417 418 private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DefaultSystemConnectionMemo.class); 419 420}