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}