001package jmri.managers.configurexml; 002 003import java.lang.reflect.Constructor; 004import java.util.List; 005 006import javax.annotation.CheckForNull; 007import javax.annotation.Nonnull; 008 009import jmri.InstanceManager; 010import jmri.Manager; 011import jmri.NamedBean; 012import jmri.NamedBeanHandle; 013import jmri.NamedBeanHandleManager; 014import jmri.configurexml.JmriConfigureXmlException; 015import jmri.configurexml.XmlAdapter; 016 017import org.jdom2.Attribute; 018import org.jdom2.Element; 019 020/** 021 * Provides services for configuring NamedBean manager storage. 022 * <p> 023 * Not a full abstract implementation by any means, rather this class provides 024 * various common service routines to eventual type-specific subclasses. 025 * 026 * @author Bob Jacobsen Copyright: Copyright (c) 2009 027 * @since 2.3.1 028 */ 029public abstract class AbstractNamedBeanManagerConfigXML extends jmri.configurexml.AbstractXmlAdapter { 030 031 static final String STR_SYSTEM_NAME = "systemName"; 032 static final String STR_USER_NAME = "userName"; 033 static final String STR_COMMENT = "comment"; 034 035 static final String STR_VALUE = "value"; 036 static final String STR_CLASS = "class"; 037 038 static final String STR_KEY = "key"; 039 static final String STR_PROPERTY = "property"; 040 static final String STR_PROPERTIES = "properties"; 041 042 public AbstractNamedBeanManagerConfigXML() { 043 } 044 045 /** 046 * Store common items: 047 * <ul> 048 * <li>user name 049 * <li>comment 050 * </ul> 051 * 052 * @param t The NamedBean being stored 053 * @param elem The JDOM element for storing the NamedBean 054 */ 055 protected void storeCommon(@Nonnull NamedBean t, Element elem) { 056 storeUserName(t, elem); 057 storeComment(t, elem); 058 storeProperties(t, elem); 059 } 060 061 /** 062 * Load common items: 063 * <ul> 064 * <li>comment 065 * </ul> 066 * The username is not loaded, because it had to be provided in the ctor 067 * earlier. 068 * 069 * @param t The NamedBean being loaded 070 * @param elem The JDOM element containing the NamedBean 071 */ 072 protected void loadCommon(NamedBean t, Element elem) { 073 loadComment(t, elem); 074 loadProperties(t, elem); 075 } 076 077 /** 078 * Store the comment parameter from a NamedBean 079 * 080 * @param t The NamedBean being stored 081 * @param elem The JDOM element for storing the NamedBean 082 */ 083 void storeComment( @Nonnull NamedBean t, @Nonnull Element elem) { 084 // add comment, if present 085 if (t.getComment() != null) { 086 Element c = new Element(STR_COMMENT); 087 c.addContent(t.getComment()); 088 elem.addContent(c); 089 } 090 } 091 092 /** 093 * Store the username parameter from a NamedBean. 094 * <ul> 095 * <li>Before 2.9.6, this was an attribute 096 * <li>Starting in 2.9.6, this was stored as both attribute and element 097 * <li>Starting in 3.1/2.11.1, this will be just an element 098 * </ul> 099 * 100 * @param t The NamedBean being stored 101 * @param elem The JDOM element for storing the NamedBean 102 */ 103 void storeUserName( @Nonnull NamedBean t, @Nonnull Element elem) { 104 String uname = t.getUserName(); 105 if ( uname != null && !uname.isEmpty()) { 106 elem.addContent(new Element(STR_USER_NAME).addContent(uname)); 107 } 108 } 109 110 /** 111 * Get the username attribute from one element of a list of Elements 112 * defining NamedBeans. 113 * 114 * @param beanList list of Elements 115 * @param i index of Element in list to examine 116 * @return the user name of bean in beanList at i or null 117 */ 118 protected String getUserName(List<Element> beanList, int i) { 119 return getUserName(beanList.get(i)); 120 } 121 122 /** 123 * Service method to load a user name, check it for validity, and if need be 124 * notify about errors. 125 * <p> 126 * The name can be empty, but if present, has to be valid. 127 * <p> 128 * There's no check to make sure the name corresponds to an existing bean, 129 * as sometimes this is used to check validity before creating the bean. 130 * <ul> 131 * <li>Before 2.9.6, this was stored as an attribute 132 * <li>Starting in 2.9.6, this was stored as both attribute and element 133 * <li>Starting in 3.1/2.11.1, this is stored as an element 134 * </ul> 135 * 136 * @param elem The existing Element 137 * @return the user name of bean or null 138 */ 139 protected String getUserName(@Nonnull Element elem) { 140 if (elem.getChild(STR_USER_NAME) != null) { 141 return elem.getChild(STR_USER_NAME).getText(); 142 } 143 if (elem.getAttribute(STR_USER_NAME) != null) { 144 return elem.getAttribute(STR_USER_NAME).getValue(); 145 } 146 return null; 147 } 148 149 /** 150 * Service method to load a system name. 151 * <p> 152 * There's no check to make sure the name corresponds to an existing bean, 153 * as sometimes this is used to check validity before creating the bean. 154 * Validity (format) checks are deferred to later, see 155 * {@link #checkNameNormalization}. 156 * <ul> 157 * <li>Before 2.9.6, this was stored as an attribute 158 * <li>Starting in 2.9.6, this was stored as both attribute and element 159 * <li>Starting in 3.1/2.10.1, this is stored as an element 160 * </ul> 161 * 162 * @param elem The existing Element 163 * @return the system name or null if not defined 164 */ 165 protected String getSystemName(@Nonnull Element elem) { 166 if (elem.getChild(STR_SYSTEM_NAME) != null) { 167 return elem.getChild(STR_SYSTEM_NAME).getText(); 168 } 169 if (elem.getAttribute(STR_SYSTEM_NAME) != null) { 170 return elem.getAttribute(STR_SYSTEM_NAME).getValue(); 171 } 172 return null; 173 } 174 175 /** 176 * Common service routine to check for and report on normalization (errors) 177 * in the incoming NamedBean's name(s) 178 * <p> 179 * If NamedBeam.normalizeUserName changes, this may want to be updated. 180 * <p> 181 * Right now, this just logs. Someday, perhaps it should notify upward of 182 * found issues by throwing an exception. 183 * <p> 184 * Package-level access to allow testing 185 * 186 * @param <T> The type of NamedBean being checked, i.e. Turnout, Sensor, etc 187 * @param rawSystemName The proposed system name string, before 188 * normalization 189 * @param rawUserName The proposed user name string, before normalization 190 * @param manager The NamedBeanManager that will be storing this 191 */ 192 <T extends NamedBean> void checkNameNormalization(@Nonnull String rawSystemName, 193 @CheckForNull String rawUserName, @Nonnull Manager<T> manager) { 194 // just check and log 195 if (rawUserName != null) { 196 String normalizedUserName = NamedBean.normalizeUserName(rawUserName); 197 if (!rawUserName.equals(normalizedUserName)) { 198 log.warn("Requested user name \"{}\" for system name \"{}\" was normalized to \"{}\"", 199 rawUserName, rawSystemName, normalizedUserName); 200 } 201 if (normalizedUserName != null) { 202 NamedBean bean = manager.getByUserName(normalizedUserName); 203 if (bean != null && !bean.getSystemName().equals(rawSystemName)) { 204 log.warn("User name \"{}\" already exists as system name \"{}\"", normalizedUserName, bean.getSystemName()); 205 } 206 } else { 207 log.warn("User name \"{}\" was normalized into null", rawUserName); 208 } 209 } 210 } 211 212 /** 213 * Service method to load a reference to a NamedBean by name, check it for 214 * validity, and if need be notify about errors. 215 * <p> 216 * The name can be empty (method returns null), but if present, has to 217 * resolve to an existing bean. 218 * 219 * @param <T> The type of NamedBean to return 220 * @param name System name, User name, empty string or null 221 * @param type A reference to the desired type, typically the name of the 222 * various being loaded, e.g. a Sensor reference 223 * @param m Manager used to check name for validity and existence 224 * @return the requested NamedBean or null if name was null 225 */ 226 public <T extends NamedBean> T checkedNamedBeanReference( 227 @CheckForNull String name, @Nonnull T type, @Nonnull Manager<T> m) { 228 if ( name == null || name.isEmpty() ) { 229 return null; 230 } 231 return m.getNamedBean(name); 232 } 233 234 /** 235 * Service method to load a NamedBeanHandle to a NamedBean by name, check it 236 * for validity, and if need be notify about errors. 237 * <p> 238 * The name can be empty (method returns null), but if present, has to 239 * resolve to an existing bean. 240 * 241 * @param <T> The type of NamedBean to return a handle for 242 * @param name System name, User name, empty string or null 243 * @param type A reference to the desired type, typically the name of the 244 * various being loaded, e.g. a Sensor reference 245 * @param m Manager used to check name for validity and existence 246 * @return a handle for the requested NamedBean or null 247 */ 248 public <T extends NamedBean> NamedBeanHandle<T> checkedNamedBeanHandle( 249 @CheckForNull String name, @Nonnull T type, @Nonnull Manager<T> m ) { 250 if ( name == null || name.isEmpty() ) { 251 return null; 252 } 253 T nb = m.getNamedBean(name); 254 if (nb == null) { 255 return null; 256 } 257 return InstanceManager.getDefault(NamedBeanHandleManager.class).getNamedBeanHandle(name, nb); 258 } 259 260 /** 261 * Service method to reference to a NamedBean by name, and if need be notify 262 * about errors. 263 * <p> 264 * The name can be empty (method returns null), but if present, has to 265 * resolve to an existing bean. or new). 266 * 267 * @param <T> The type of the NamedBean 268 * @param name System name, User name, empty string or null 269 * @param type A reference to the desired type, typically the name of the 270 * various being loaded, e.g. a Sensor reference; may have null 271 * value, but has to be typed 272 * @param m Manager used to check name for validity and existence 273 * @return name if a matching NamedBean can be found or null 274 */ 275 public <T extends NamedBean> String checkedNamedBeanName(@CheckForNull String name, T type, @Nonnull Manager<T> m) { 276 if ( name == null || name.isEmpty() ) { 277 return null; 278 } 279 NamedBean nb = m.getNamedBean(name); 280 if (nb == null) { 281 return null; 282 } 283 return name; 284 } 285 286 /** 287 * Load the comment attribute into a NamedBean from one element of a list of 288 * Elements defining NamedBeans 289 * 290 * @param t The NamedBean being loaded 291 * @param beanList List, where each entry is an Element 292 * @param i index of Element in list to examine 293 */ 294 void loadComment(NamedBean t, List<Element> beanList, int i) { 295 loadComment(t, beanList.get(i)); 296 } 297 298 /** 299 * Load the comment attribute into a NamedBean from an Element defining a 300 * NamedBean 301 * 302 * @param t The NamedBean being loaded 303 * @param elem The existing Element 304 */ 305 void loadComment(NamedBean t, @Nonnull Element elem) { 306 // load comment, if present 307 String c = elem.getChildText(STR_COMMENT); 308 if (c != null) { 309 t.setComment(c); 310 } 311 } 312 313 /** 314 * Convenience method to get a String value from an Attribute in an Element 315 * defining a NamedBean. 316 * 317 * @param elem existing Element 318 * @param name name of desired Attribute 319 * @return attribute value or null if name is not an attribute of elem 320 */ 321 String getAttributeString( @Nonnull Element elem, String name) { 322 Attribute a = elem.getAttribute(name); 323 if (a != null) { 324 return a.getValue(); 325 } else { 326 return null; 327 } 328 } 329 330 /** 331 * Convenience method to get a boolean value from an Attribute in an Element 332 * defining a NamedBean. 333 * 334 * @param elem existing Element 335 * @param name name of desired Attribute 336 * @param def default value for attribute 337 * @return value of attribute name or def if name is not an attribute of 338 * elem 339 */ 340 boolean getAttributeBool( @Nonnull Element elem, String name, boolean def) { 341 String v = getAttributeString(elem, name); 342 if (v == null) { 343 return def; 344 } else if (def) { 345 return !v.equals(STR_FALSE); 346 } else { 347 return v.equals(STR_TRUE); 348 } 349 } 350 351 /** 352 * Store all key/value properties. 353 * 354 * @param t The NamedBean being loaded 355 * @param elem The existing Element 356 */ 357 void storeProperties( @Nonnull NamedBean t, @Nonnull Element elem) { 358 java.util.Set<String> s = t.getPropertyKeys(); 359 if (s.isEmpty()) { 360 return; 361 } 362 Element ret = new Element(STR_PROPERTIES); 363 elem.addContent(ret); 364 s.forEach( key -> { 365 Object value = t.getProperty(key); 366 Element p = new Element(STR_PROPERTY); 367 ret.addContent(p); 368 p.addContent(new Element(STR_KEY).setText(key)); 369 if (value != null) { 370 p.addContent(new Element(STR_VALUE) 371 .setAttribute(STR_CLASS, value.getClass().getName()) 372 .setText(value.toString()) 373 ); 374 } 375 }); 376 } 377 378 /** 379 * Load all key/value properties 380 * 381 * @param t The NamedBean being loaded 382 * @param elem The existing Element 383 */ 384 void loadProperties(NamedBean t, Element elem) { 385 Element p = elem.getChild(STR_PROPERTIES); 386 if (p == null) { 387 return; 388 } 389 p.getChildren(STR_PROPERTY).forEach( e -> { 390 try { 391 Class<?> cl; 392 Constructor<?> ctor; 393 394 // create key string 395 String key = e.getChild("key").getText(); 396 397 // check for non-String key. Warn&proceed if found. 398 // Pre-JMRI 4.3, keys in NamedBean parameters could be Objects 399 // constructed from Strings, similar to the value code below. 400 if (!(e.getChild(STR_KEY).getAttributeValue(STR_CLASS) == null 401 || e.getChild(STR_KEY).getAttributeValue(STR_CLASS).isEmpty() 402 || e.getChild(STR_KEY).getAttributeValue(STR_CLASS).equals("java.lang.String"))) { 403 404 log.warn("NamedBean {} property key of invalid non-String type {} not supported", 405 t.getSystemName(), e.getChild("key").getAttributeValue(STR_CLASS)); 406 } 407 408 // create value object 409 Object value = null; 410 if (e.getChild(STR_VALUE) != null) { 411 cl = Class.forName(e.getChild(STR_VALUE).getAttributeValue(STR_CLASS)); 412 ctor = cl.getConstructor(String.class); 413 value = ctor.newInstance(e.getChild(STR_VALUE).getText()); 414 } 415 416 // store 417 t.setProperty(key, value); 418 } catch (ClassNotFoundException | NoSuchMethodException 419 | InstantiationException | IllegalAccessException 420 | java.lang.reflect.InvocationTargetException ex) { 421 log.error("Error loading properties", ex); 422 } 423 }); 424 } 425 426 /** 427 * Load all attribute properties from a list. 428 * TODO make abstract (remove logging) and move method to XmlAdapter so it can be used from PanelEditorXml et al 429 * 430 * @param list list of Elements read from xml 431 * @param perNode Top-level XML element containing the private, single-node elements of the description. 432 * always null in current application, included to use for Element panel in jmri.jmrit.display 433 * @return true if the load was successful 434 */ 435 boolean loadInAdapter(List<Element> list, Element perNode) { 436 boolean result = true; 437 for (Element item : list) { 438 // get the class, hence the adapter object to do loading 439 String adapterName = item.getAttribute(STR_CLASS).getValue(); 440 log.debug("load via {}", adapterName); 441 try { 442 XmlAdapter adapter = (XmlAdapter) Class.forName(adapterName).getDeclaredConstructor().newInstance(); 443 // and do it 444 adapter.load(item, perNode); 445 } catch (ClassNotFoundException | NoSuchMethodException | InstantiationException 446 | IllegalAccessException | java.lang.reflect.InvocationTargetException 447 | JmriConfigureXmlException e) { 448 log.error("Exception while loading {}: {}", item.getName(), e, e); 449 result = false; 450 } 451 } 452 return result; 453 } 454 455 private static final org.slf4j.Logger log = 456 org.slf4j.LoggerFactory.getLogger(AbstractNamedBeanManagerConfigXML.class); 457 458}