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