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