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}