001package jmri.jmrix; 002 003import java.io.IOException; 004import java.io.InputStream; 005import java.util.ArrayList; 006import java.util.Arrays; 007import java.util.HashMap; 008import java.util.HashSet; 009import java.util.Iterator; 010import java.util.List; 011import java.util.Locale; 012import java.util.Properties; 013import java.util.ServiceLoader; 014import java.util.Set; 015import java.util.TreeSet; 016import javax.annotation.CheckForNull; 017import javax.annotation.Nonnull; 018import jmri.InstanceManager; 019import jmri.configurexml.ClassMigrationManager; 020import jmri.configurexml.ConfigXmlManager; 021import jmri.configurexml.ErrorHandler; 022import jmri.configurexml.ErrorMemo; 023import jmri.configurexml.XmlAdapter; 024import jmri.jmrix.internal.InternalConnectionTypeList; 025import jmri.profile.Profile; 026import jmri.profile.ProfileUtils; 027import jmri.spi.PreferencesManager; 028import jmri.util.jdom.JDOMUtil; 029import jmri.util.prefs.AbstractPreferencesManager; 030import jmri.util.prefs.HasConnectionButUnableToConnectException; 031import org.jdom2.Element; 032import org.jdom2.JDOMException; 033import org.openide.util.lookup.ServiceProvider; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037/** 038 * Manager for ConnectionConfig objects. 039 * 040 * @author Randall Wood (C) 2015 041 */ 042@ServiceProvider(service = PreferencesManager.class) 043public class ConnectionConfigManager extends AbstractPreferencesManager implements Iterable<ConnectionConfig> { 044 045 private final ArrayList<ConnectionConfig> connections = new ArrayList<>(); 046 private final static String NAMESPACE = "http://jmri.org/xml/schema/auxiliary-configuration/connections-2-9-6.xsd"; // NOI18N 047 public final static String CONNECTIONS = "connections"; // NOI18N 048 public final static String CONNECTION = "connection"; // NOI18N 049 public final static String CLASS = "class"; // NOI18N 050 public final static String USER_NAME = "userName"; // NOI18N 051 public final static String SYSTEM_NAME = "systemPrefix"; // NOI18N 052 public final static String MANUFACTURER = "manufacturer"; // NOI18N 053 private final static Logger log = LoggerFactory.getLogger(ConnectionConfigManager.class); 054 055 @Override 056 public void initialize(Profile profile) throws HasConnectionButUnableToConnectException { 057 if (!isInitialized(profile)) { 058 log.debug("Initializing..."); 059 Element sharedConnections = null; 060 Element perNodeConnections = null; 061 this.setPortNamePattern(); 062 try { 063 sharedConnections = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(profile).getConfigurationFragment(CONNECTIONS, NAMESPACE, true)); 064 } catch (NullPointerException ex) { 065 // Normal if this is a new profile 066 log.info("No connections configured."); 067 log.debug("Null pointer thrown reading shared configuration.", ex); 068 } 069 if (sharedConnections != null) { 070 try { 071 perNodeConnections = JDOMUtil.toJDOMElement(ProfileUtils.getAuxiliaryConfiguration(profile).getConfigurationFragment(CONNECTIONS, NAMESPACE, false)); 072 } catch (NullPointerException ex) { 073 // Normal if the profile has not been used on this computer 074 log.info("No local configuration found."); 075 log.debug("Null pointer thrown reading local configuration.", ex); 076 // TODO: notify user 077 } 078 for (Element shared : sharedConnections.getChildren(CONNECTION)) { 079 Element perNode = shared; 080 String className = shared.getAttributeValue(CLASS); 081 String userName = shared.getAttributeValue(USER_NAME, ""); // NOI18N 082 String systemName = shared.getAttributeValue(SYSTEM_NAME, ""); // NOI18N 083 String manufacturer = shared.getAttributeValue(MANUFACTURER, ""); // NOI18N 084 log.debug("Read shared connection {}:{} ({}) class {}", userName, systemName, manufacturer, className); 085 if (perNodeConnections != null) { 086 for (Element e : perNodeConnections.getChildren(CONNECTION)) { 087 if (systemName.equals(e.getAttributeValue(SYSTEM_NAME))) { 088 perNode = e; 089 className = perNode.getAttributeValue(CLASS); 090 userName = perNode.getAttributeValue(USER_NAME, ""); // NOI18N 091 manufacturer = perNode.getAttributeValue(MANUFACTURER, ""); // NOI18N 092 log.debug("Read perNode connection {}:{} ({}) class {}", userName, systemName, manufacturer, className); 093 } 094 } 095 } 096 String newClassName = InstanceManager.getDefault(ClassMigrationManager.class).getClassName(className); 097 if (!className.equals(newClassName)) { 098 log.info("Class {} will be used for connection {} instead of {} if preferences are saved", newClassName, userName, className); 099 className = newClassName; 100 } 101 try { 102 log.debug("Creating connection {}:{} ({}) class {}", userName, systemName, manufacturer, className); 103 XmlAdapter adapter = (XmlAdapter) Class.forName(className).getDeclaredConstructor().newInstance(); 104 ConnectionConfigManagerErrorHandler handler = new ConnectionConfigManagerErrorHandler(); 105 adapter.setExceptionHandler(handler); 106 if (!adapter.load(shared, perNode)) { 107 log.error("Unable to create {} for {}, load returned false", className, shared); 108 String english = Bundle.getMessage(Locale.ENGLISH, "ErrorSingleConnection", userName, systemName); // NOI18N 109 String localized = Bundle.getMessage("ErrorSingleConnection", userName, systemName); // NOI18N 110 this.addInitializationException(profile, new HasConnectionButUnableToConnectException(english, localized)); 111 } 112 handler.exceptions.forEach((exception) -> { 113 this.addInitializationException(profile, exception); 114 }); 115 } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | java.lang.reflect.InvocationTargetException ex) { 116 log.error("Unable to create {} for {}", className, shared, ex); 117 String english = Bundle.getMessage(Locale.ENGLISH, "ErrorSingleConnection", userName, systemName); // NOI18N 118 String localized = Bundle.getMessage("ErrorSingleConnection", userName, systemName); // NOI18N 119 this.addInitializationException(profile, new HasConnectionButUnableToConnectException(english, localized, ex)); 120 } catch (RuntimeException | jmri.configurexml.JmriConfigureXmlException ex) { 121 log.error("Unable to load {} into {}", shared, className, ex); 122 String english = Bundle.getMessage(Locale.ENGLISH, "ErrorSingleConnection", userName, systemName); // NOI18N 123 String localized = Bundle.getMessage("ErrorSingleConnection", userName, systemName); // NOI18N 124 this.addInitializationException(profile, new HasConnectionButUnableToConnectException(english, localized, ex)); 125 } 126 } 127 } 128 setInitialized(profile, true); 129 List<Exception> exceptions = this.getInitializationExceptions(profile); 130 if (exceptions.size() == 1) { 131 if (exceptions.get(0) instanceof HasConnectionButUnableToConnectException) { 132 throw (HasConnectionButUnableToConnectException) exceptions.get(0); 133 } else { 134 throw new HasConnectionButUnableToConnectException(exceptions.get(0)); 135 } 136 } else if (exceptions.size() > 1) { 137 String english = Bundle.getMessage(Locale.ENGLISH, "ErrorMultipleConnections"); // NOI18N 138 String localized = Bundle.getMessage("ErrorMultipleConnections"); // NOI18N 139 throw new HasConnectionButUnableToConnectException(english, localized); 140 } 141 log.debug("Initialized..."); 142 } 143 } 144 145 @Override 146 @Nonnull 147 public Set<Class<? extends PreferencesManager>> getRequires() { 148 return new HashSet<>(); 149 } 150 151 @Override 152 public void savePreferences(Profile profile) { 153 log.debug("Saving connections preferences..."); 154 // store shared Connection preferences 155 savePreferences(profile, true); 156 // store private or perNode Connection preferences 157 savePreferences(profile, false); 158 log.debug("Saved connections preferences..."); 159 } 160 161 private synchronized void savePreferences(Profile profile, boolean shared) { 162 Element element = new Element(CONNECTIONS, NAMESPACE); 163 connections.stream().forEach((o) -> { 164 log.debug("Saving connection {} ({})...", o.getConnectionName(), shared); 165 Element e = ConfigXmlManager.elementFromObject(o, shared); 166 if (e != null) { 167 element.addContent(e); 168 } 169 }); 170 // save connections, or save an empty connections element if user removed all connections 171 try { 172 ProfileUtils.getAuxiliaryConfiguration(profile).putConfigurationFragment(JDOMUtil.toW3CElement(element), shared); 173 } catch (JDOMException ex) { 174 log.error("Unable to create create XML", ex); 175 } 176 } 177 178 /** 179 * Add a {@link jmri.jmrix.ConnectionConfig} following the rules specified 180 * in {@link java.util.Collection#add(java.lang.Object)}. 181 * 182 * @param c an existing ConnectionConfig 183 * @return true if c was added, false otherwise 184 * @throws NullPointerException if c is null 185 */ 186 public boolean add(@Nonnull ConnectionConfig c) throws NullPointerException { 187 if (c == null) { 188 throw new NullPointerException(); 189 } 190 if (!connections.contains(c)) { 191 boolean result = connections.add(c); 192 int i = connections.indexOf(c); 193 fireIndexedPropertyChange(CONNECTIONS, i, null, c); 194 return result; 195 } 196 return false; 197 } 198 199 /** 200 * Remove a {@link jmri.jmrix.ConnectionConfig} following the rules 201 * specified in {@link java.util.Collection#add(java.lang.Object)}. 202 * 203 * @param c an existing ConnectionConfig 204 * @return true if c was removed, false otherwise 205 */ 206 public boolean remove(@Nonnull ConnectionConfig c) { 207 int i = connections.indexOf(c); 208 boolean result = connections.remove(c); 209 if (result) { 210 fireIndexedPropertyChange(CONNECTIONS, i, c, null); 211 } 212 return result; 213 } 214 215 /** 216 * Get an Array of {@link jmri.jmrix.ConnectionConfig} objects. 217 * 218 * @return an Array, possibly empty if there are no ConnectionConfig 219 * objects. 220 */ 221 @Nonnull 222 public ConnectionConfig[] getConnections() { 223 return connections.toArray(new ConnectionConfig[connections.size()]); 224 } 225 226 /** 227 * Get the {@link jmri.jmrix.ConnectionConfig} at index following the rules 228 * specified in {@link java.util.Collection#add(java.lang.Object)}. 229 * 230 * @param index index of the ConnectionConfig to return 231 * @return the ConnectionConfig at the specified location 232 */ 233 public ConnectionConfig getConnections(int index) { 234 return connections.get(index); 235 } 236 237 @Override 238 public Iterator<ConnectionConfig> iterator() { 239 return connections.iterator(); 240 } 241 242 /** 243 * Get the class names for classes supporting layout connections for the 244 * given manufacturer. 245 * 246 * @param manufacturer the name of the manufacturer 247 * @return An array of supporting class names; will return the list of 248 * internal connection classes if manufacturer is not a known 249 * manufacturer; the array may be empty if there are no supporting 250 * classes for the given manufacturer. 251 */ 252 @Nonnull 253 public String[] getConnectionTypes(@Nonnull String manufacturer) { 254 return this.getDefaultConnectionTypeManager().getConnectionTypes(manufacturer); 255 } 256 257 /** 258 * Get the list of known manufacturers. 259 * 260 * @return An array of known manufacturers. 261 */ 262 @Nonnull 263 public String[] getConnectionManufacturers() { 264 return this.getDefaultConnectionTypeManager().getConnectionManufacturers(); 265 } 266 267 /** 268 * Get the manufacturer that is supported by a connection type. If there are 269 * multiple manufacturers supported by connectionType, returns only the 270 * first manufacturer. 271 * 272 * @param connectionType the class name of a connection type. 273 * @return the supported manufacturer. Returns null if no manufacturer is 274 * associated with the connectionType. 275 */ 276 @CheckForNull 277 public String getConnectionManufacturer(@Nonnull String connectionType) { 278 for (String manufacturer : this.getConnectionManufacturers()) { 279 for (String manufacturerType : this.getConnectionTypes(manufacturer)) { 280 if (connectionType.equals(manufacturerType)) { 281 return manufacturer; 282 } 283 } 284 } 285 return null; 286 } 287 288 /** 289 * Get the list of all known manufacturers that a single connection type 290 * supports. 291 * 292 * @param connectionType the class name of a connection type. 293 * @return an Array of supported manufacturers. Returns an empty Array if no 294 * manufacturer is associated with the connectionType. 295 */ 296 @Nonnull 297 public String[] getConnectionManufacturers(@Nonnull String connectionType) { 298 ArrayList<String> manufacturers = new ArrayList<>(); 299 for (String manufacturer : this.getConnectionManufacturers()) { 300 for (String manufacturerType : this.getConnectionTypes(manufacturer)) { 301 if (connectionType.equals(manufacturerType)) { 302 manufacturers.add(manufacturer); 303 } 304 } 305 } 306 return manufacturers.toArray(new String[manufacturers.size()]); 307 } 308 309 /** 310 * Get the default {@link ConnectionTypeManager}, creating it if needed. 311 * 312 * @return the default ConnectionTypeManager 313 */ 314 private ConnectionTypeManager getDefaultConnectionTypeManager() { 315 if (InstanceManager.getNullableDefault(ConnectionTypeManager.class) == null) { 316 InstanceManager.setDefault(ConnectionTypeManager.class, new ConnectionTypeManager()); 317 } 318 return InstanceManager.getDefault(ConnectionTypeManager.class); 319 } 320 321 private static class ConnectionTypeManager { 322 323 private final HashMap<String, ConnectionTypeList> connectionTypeLists = new HashMap<>(); 324 325 public ConnectionTypeManager() { 326 for (ConnectionTypeList ctl : ServiceLoader.load(ConnectionTypeList.class)) { 327 for (String manufacturer : ctl.getManufacturers()) { 328 if (!connectionTypeLists.containsKey(manufacturer)) { 329 connectionTypeLists.put(manufacturer, ctl); 330 log.debug("Added {} connectionTypeList", manufacturer); 331 } else { 332 log.debug("Need a proxy for {} from {} in {}", manufacturer, ctl.getClass().getName(), this); 333 ProxyConnectionTypeList proxy; 334 ConnectionTypeList existing = connectionTypeLists.get(manufacturer); 335 if (existing instanceof ProxyConnectionTypeList) { 336 proxy = (ProxyConnectionTypeList) existing; 337 } else { 338 proxy = new ProxyConnectionTypeList(existing); 339 } 340 proxy.add(ctl); 341 connectionTypeLists.put(manufacturer, proxy); 342 } 343 } 344 } 345 } 346 347 public String[] getConnectionTypes(String manufacturer) { 348 ConnectionTypeList ctl = this.connectionTypeLists.get(manufacturer); 349 if (ctl != null) { 350 return ctl.getAvailableProtocolClasses(); 351 } 352 return this.connectionTypeLists.get(InternalConnectionTypeList.NONE).getAvailableProtocolClasses(); 353 } 354 355 public String[] getConnectionManufacturers() { 356 ArrayList<String> a = new ArrayList<>(this.connectionTypeLists.keySet()); 357 a.remove(InternalConnectionTypeList.NONE); 358 a.sort(null); 359 a.add(0, InternalConnectionTypeList.NONE); 360 return a.toArray(new String[a.size()]); 361 } 362 363 } 364 365 private static class ProxyConnectionTypeList implements ConnectionTypeList { 366 367 private final ArrayList<ConnectionTypeList> connectionTypeLists = new ArrayList<>(); 368 369 public ProxyConnectionTypeList(@Nonnull ConnectionTypeList connectionTypeList) { 370 log.debug("Creating proxy for {}", connectionTypeList.getManufacturers()[0]); 371 this.add(connectionTypeList); 372 } 373 374 public final void add(@Nonnull ConnectionTypeList connectionTypeList) { 375 log.debug("Adding {} to proxy", connectionTypeList.getClass().getName()); 376 this.connectionTypeLists.add(connectionTypeList); 377 } 378 379 @Override 380 @Nonnull 381 public String[] getAvailableProtocolClasses() { 382 TreeSet<String> classes = new TreeSet<>(); 383 this.connectionTypeLists.stream().forEach((connectionTypeList) -> { 384 classes.addAll(Arrays.asList(connectionTypeList.getAvailableProtocolClasses())); 385 }); 386 return classes.toArray(new String[classes.size()]); 387 } 388 389 @Override 390 @Nonnull 391 public String[] getManufacturers() { 392 TreeSet<String> manufacturers = new TreeSet<>(); 393 this.connectionTypeLists.stream().forEach((connectionTypeList) -> { 394 manufacturers.addAll(Arrays.asList(connectionTypeList.getManufacturers())); 395 }); 396 return manufacturers.toArray(new String[manufacturers.size()]); 397 } 398 399 } 400 401 /** 402 * Override the default port name patterns unless the 403 * purejavacomm.portnamepattern property was set on the command line. 404 */ 405 private void setPortNamePattern() { 406 final String pattern = "purejavacomm.portnamepattern"; 407 Properties properties = System.getProperties(); 408 if (properties.getProperty(pattern) == null) { 409 try (InputStream in = ConnectionConfigManager.class.getResourceAsStream("PortNamePatterns.properties")) { // NOI18N 410 properties.load(in); 411 } catch (IOException ex) { 412 log.error("Unable to read PortNamePatterns.properties", ex); 413 } 414 } 415 } 416 417 private static class ConnectionConfigManagerErrorHandler extends ErrorHandler { 418 419 ArrayList<HasConnectionButUnableToConnectException> exceptions = new ArrayList<>(); 420 421 public ConnectionConfigManagerErrorHandler() { 422 super(); 423 } 424 425 /** 426 * Capture ErrorMemos as initialization exceptions. {@inheritDoc} 427 */ 428 @Override 429 // The memo has a generic message (since the real cause never makes it this far anyway) 430 // If the memo reliably had an exception, we could make a decision about 431 // how to handle that, but since it doesn't all we can do is log it 432 public void handle(ErrorMemo memo) { 433 if (memo.exception != null) { 434 this.exceptions.add(new HasConnectionButUnableToConnectException(memo.description, Bundle.getMessage("ErrorSubException", memo.description), memo.exception)); 435 } else { 436 this.exceptions.add(new HasConnectionButUnableToConnectException(memo.description, Bundle.getMessage("ErrorSubException", memo.description) + memo.description)); 437 } 438 } 439 } 440 441}