001package jmri.configurexml;
002
003import javax.annotation.CheckForNull;
004import javax.annotation.Nonnull;
005import java.util.HashMap;
006import java.util.Map;
007import org.jdom2.Attribute;
008import org.jdom2.Element;
009
010/**
011 * Abstract class to provide basic error handling for XmlAdapter
012 *
013 * @author Bob Jacobsen Copyright (c) 2009
014 * @see XmlAdapter
015 */
016public abstract class AbstractXmlAdapter implements XmlAdapter {
017
018    private ErrorHandler errorHandler = XmlAdapter.getDefaultExceptionHandler();
019
020    public static final String STR_TRUE = "true";
021    public static final String STR_FALSE = "false";
022    public static final String STR_YES = "yes";
023    public static final String STR_NO = "no";
024
025    /**
026     * Return the value of a named child element, or an empty string if the
027     * child is missing.
028     * @param parent parent element
029     * @param childName child element name
030     * @return child value, or an empty string
031     */
032    protected String childValue(Element parent, String childName) {
033        Element child = parent.getChild(childName);
034        return child == null ? "" : child.getValue();
035    }
036
037    /**
038     * Return an empty string for a null value.
039     * @param value value to convert
040     * @return value, or an empty string if null
041     */
042    protected String nullToEmpty(String value) {
043        return value == null ? "" : value;
044    }
045
046    /** {@inheritDoc} */
047    @Override
048    public void handleException(
049            String description,
050            String operation,
051            String systemName,
052            String userName,
053            Exception exception) {
054        if (errorHandler != null) {
055            this.errorHandler.handle(new ErrorMemo(this, operation, description, systemName, userName, exception));
056        }
057    }
058
059    /** {@inheritDoc} */
060    @Override
061    public boolean load(Element e) throws JmriConfigureXmlException {
062        throw new UnsupportedOperationException("One of the other load methods must be implemented.");
063    }
064
065    /** {@inheritDoc} */
066    @Override
067    public void load(Element e, Object o) throws JmriConfigureXmlException {
068        log.error("Invalid method called");
069    }
070
071    /** {@inheritDoc} */
072    @Override
073    public boolean load(@Nonnull Element shared, Element perNode) throws JmriConfigureXmlException { // may not need exception
074        return this.load(shared);
075    }
076
077    /** {@inheritDoc} */
078    @Override
079    public void load(@Nonnull Element shared, Element perNode, Object o) throws JmriConfigureXmlException { // may not need exception
080        this.load(shared, o);
081    }
082
083    /**
084     * Determine if this set of configured objects should be loaded after basic
085     * GUI construction is completed.
086     * <p>
087     * Default behavior is to load when requested. Classes that should wait
088     * until basic GUI is constructed should override this method and return
089     * true
090     *
091     * @return true to defer loading
092     * @see jmri.configurexml.XmlAdapter#loadDeferred()
093     * @since 2.11.2
094     */
095    @Override
096    public boolean loadDeferred() {
097        return false;
098    }
099
100    /** {@inheritDoc} */
101    @Override
102    public int loadOrder() {
103        return 50;
104    }
105
106    /** {@inheritDoc} */
107    @Override
108    public Element store(@Nonnull Object o, boolean shared) {
109        if (shared) {
110            return this.store(o);
111        }
112        return null;
113    }
114
115    /** {@inheritDoc} */
116    @Override
117    public void setExceptionHandler(ErrorHandler errorHandler) {
118        this.errorHandler = errorHandler;
119    }
120
121    /** {@inheritDoc} */
122    @Override
123    public ErrorHandler getExceptionHandler() {
124        return this.errorHandler;
125    }
126
127    /**
128     * Service method to handle attribute input of
129     * boolean  (true/yes vs false/no) values.  Not being present
130     * is not an error. Not parsing (which shouldn't happen due to
131     * the XML Schema checks) invokes the default error handler.
132     * @param element the element to parse.
133     * @param name element attribute name.
134     * @param def default value if name not present in element.
135     * @return boolean value of attribute, else default if not present or error.
136     */
137    public final boolean getAttributeBooleanValue(@Nonnull Element element, @Nonnull String name, boolean def) {
138        Attribute a;
139        String val = null;
140        try {
141            a = element.getAttribute(name);
142            if (a == null) {
143                return def;
144            }
145            val = a.getValue();
146            if ( val.equals(STR_YES) || val.equals(STR_TRUE) ) {
147                return true;
148            }
149            if ( val.equals(STR_NO) || val.equals(STR_FALSE) ) {
150                return false;
151            }
152            return def;
153        } catch (Exception ex) {
154            log.debug("caught exception", ex);
155            ErrorMemo em = new ErrorMemo(this,
156                                            "getAttributeBooleanValue threw exception",
157                                            "element: "+element.getName(),
158                                            "attribute: "+name,
159                                            "value: "+val,
160                                            ex);
161            getExceptionHandler().handle(em);
162            return def;
163        }
164    }
165
166    /**
167     * Service method to handle attribute input of
168     * integer values.  Not being present
169     * is not an error. Not parsing (which shouldn't happen due to
170     * the XML Schema checks) invokes the default error handler.
171     * @param element the element to parse.
172     * @param name element attribute name.
173     * @param def default value if name not present in element.
174     * @return integer value of attribute, else default if not present or error.
175     */
176    public final int getAttributeIntegerValue(@Nonnull Element element, @Nonnull String name, int def) {
177        Attribute a;
178        String val = null;
179        try {
180            a = element.getAttribute(name);
181            if (a == null) {
182                return def;
183            }
184            val = a.getValue();
185            return a.getIntValue();
186        } catch (Exception ex) {
187            log.debug("caught exception", ex);
188            ErrorMemo em = new ErrorMemo(this,
189                                            "getAttributeIntegerValue threw exception",
190                                            "element: "+element.getName(),
191                                            "attribute: "+name,
192                                            "value: "+val,
193                                            ex);
194            getExceptionHandler().handle(em);
195            return def;
196        }
197    }
198
199    /**
200     * Service method to handle attribute input of
201     * double values.  Not being present
202     * is not an error. Not parsing (which shouldn't happen due to
203     * the XML Schema checks) invokes the default error handler.
204     * @param element the element to parse.
205     * @param name element attribute name.
206     * @param def default value if name not present in element.
207     * @return double value of attribute, else default if not present or error.
208     */
209    public final double getAttributeDoubleValue(@Nonnull Element element, @Nonnull String name, double def) {
210        Attribute a;
211        String val = null;
212        try {
213            a = element.getAttribute(name);
214            if (a == null) {
215                return def;
216            }
217            val = a.getValue();
218            return a.getDoubleValue();
219        } catch (Exception ex) {
220            log.debug("caught exception", ex);
221            ErrorMemo em = new ErrorMemo(this,
222                                            "getAttributeDoubleValue threw exception",
223                                            "element: "+element.getName(),
224                                            "attribute: "+name,
225                                            "value: "+val,
226                                            ex);
227            getExceptionHandler().handle(em);
228            return def;
229        }
230    }
231
232    /**
233     * Service method to handle attribute input of
234     * float values.  Not being present
235     * is not an error. Not parsing (which shouldn't happen due to
236     * the XML Schema checks) invokes the default error handler.
237     * 
238     * @param element the element to parse.
239     * @param name element attribute name.
240     * @param def default value if name not present in element.
241     * @return float value of attribute, else default if not present or error.
242     */
243    public final float getAttributeFloatValue(@Nonnull Element element, @Nonnull String name, float def) {
244        Attribute a;
245        String val = null;
246        try {
247            a = element.getAttribute(name);
248            if (a == null) {
249                return def;
250            }
251            val = a.getValue();
252            return a.getFloatValue();
253        } catch (Exception ex) {
254            log.debug("caught exception", ex);
255            ErrorMemo em = new ErrorMemo(this,
256                                            "getAttributeFloatValue threw exception",
257                                            "element: "+element.getName(),
258                                            "attribute: "+name,
259                                            "value: "+val,
260                                            ex);
261            getExceptionHandler().handle(em);
262            return def;
263        }
264    }
265
266    /**
267     * Base for support of Enum load/store to XML files.
268     * <p>
269     * Public to be usable by adapters in other configXML packages.
270     * @param <T> The ENUM implementation type.
271     */
272    public abstract static class EnumIO <T extends Enum<T>> {
273
274        /**
275         * Convert an enum value to a String for storage in an XML file.
276         * @param e enum value.
277         * @return storage string.
278         */
279        @Nonnull
280        public abstract String outputFromEnum(@Nonnull T e);
281        
282        /**
283         * Convert a String value from an XML file to an enum value.
284         * @param s storage string
285         * @return enum value.
286         */
287        @Nonnull
288        public abstract T inputFromString(@CheckForNull String s);
289
290        /**
291         * Convert a JDOM Attribute from an XML file to an enum value
292         * @param a JDOM attribute.
293         * @return enum value.
294         */
295        @Nonnull
296        public T inputFromAttribute(@Nonnull Attribute a) {
297            return inputFromString(a.getValue());
298        }
299    }
300    
301    /**
302     * Support for Enum I/O to XML using the enum's ordinal numbers in String form.<p>
303     * String or mapped I/LO should he preferred.<p>
304     * This converts to and from ordinal numbers
305     * so the order of definitions in the enum has to 
306     * match up with the (former) constant values.
307     * <p>
308     * Public to be usable by adapters in other configXML packages.
309     * @param <T> generic Enum class.
310     */
311    public static class EnumIoOrdinals <T extends Enum<T>> extends EnumIO<T> {
312    
313        public EnumIoOrdinals(@Nonnull Class<T> clazz) {
314            this.clazz = clazz;
315        }
316        final Class<T> clazz;
317
318        /** {@inheritDoc} */
319        @Override
320        @Nonnull
321        public String outputFromEnum(@Nonnull T e) {
322            int ordinal = e.ordinal();
323            return Integer.toString(ordinal);
324        }
325        
326        /** {@inheritDoc} */
327        @Override
328        @Nonnull
329        public T inputFromString(@CheckForNull String s) {
330            if (s == null) {
331                log.error("from String null get {} for {}", clazz.getEnumConstants()[0].name(), clazz);
332                return clazz.getEnumConstants()[0];
333            }
334            
335            try {
336                int content = Integer.parseInt(s);
337                return clazz.getEnumConstants()[content];
338            } catch (RuntimeException e) {
339                log.error("from String {} get {} for {}", s, clazz.getEnumConstants()[0].name(), clazz, e);
340                return clazz.getEnumConstants()[0];
341            }
342        }
343
344    }
345
346    /**
347     * Support for Enum I/O to XML using the enum's element names.
348     * @param <T> generic enum class.
349     */
350    public static class EnumIoNames <T extends Enum<T>> extends EnumIO<T> { // public to be usable by adapters in other configXML packages
351    
352        /**
353         * This constructor converts to and from strings
354         * using the enum element names.
355         * @param clazz enum class.
356         */
357        public EnumIoNames(@Nonnull Class<T> clazz) {
358            this.clazz = clazz;
359            
360            mapToEnum = new HashMap<>();
361            for (T t : clazz.getEnumConstants() ) {
362                mapToEnum.put(t.name(), t);
363            }
364            
365        }
366
367        final Class<T> clazz;
368        final Map<String, T> mapToEnum;
369        
370        /** {@inheritDoc} */
371        @Override
372        @Nonnull
373        public String outputFromEnum(@Nonnull T e) {
374            String retval = e.name();
375            log.trace("from {} make String {} for {}", e, retval, clazz);
376            return retval;
377        }
378        
379        /** {@inheritDoc} */
380        @Override
381        @Nonnull
382        public T inputFromString(@CheckForNull String s) {
383            if (s == null) {
384                log.error("from String null get {} for {}", clazz.getEnumConstants()[0].name(), clazz);
385                return clazz.getEnumConstants()[0];
386            }
387            
388            T retval = mapToEnum.get(s);
389            if (retval == null) {
390                log.error("from String {} get {} for {}", s, clazz.getEnumConstants()[0].name(), clazz);
391                return clazz.getEnumConstants()[0];
392            } else {
393                log.trace("from String {} get {} for {}", s, retval, clazz);
394                return retval;
395            }
396        }
397    }
398
399    /**
400     * Support for Enum I/O to XML using the enum's element names;
401     * for backward compatibility, it will also accept ordinal 
402     * numbers when reading.
403     * @param <T> generic enum class.
404     */
405    public static class EnumIoNamesNumbers <T extends Enum<T>> extends EnumIoNames<T> { // public to be usable by adapters in other configXML packages
406    
407        /**
408         * This constructor converts to and from strings
409         * using the enum element names and, on read only, ordinal numbers
410         * @param clazz enum class type.
411         */
412        public EnumIoNamesNumbers(@Nonnull Class<T> clazz) {
413            super(clazz);
414            
415            for (T t : clazz.getEnumConstants() ) { // append to existing map
416                mapToEnum.put(Integer.toString(t.ordinal()), t);
417            }
418            
419        }
420    }
421
422    /**
423     * Support for Enum I/O to XML using explicit mapping.<p>
424     * This converts to and from ordinal numbers
425     * so the order of definitions in the enum has to 
426     * match up with the (former) constant values.
427     * @param <T> generic enum class.
428     */
429    public static class EnumIoMapped <T extends Enum<T>> extends EnumIO<T> { // public to be usable by adapters in other configXML packages
430    
431        /**
432         * @param clazz enum class.
433         * @param mapToEnum Substitutes an explicit mapping
434         * for mapping from Strings to enums; this could allow e.g.
435         * accepting both name and number versions. Multiple entries
436         * are OK: this can map both "1" and "Foo" to Foo for past-schema support.
437         * @param mapFromEnum Substitutes an explicit mapping
438         * enum entries to Strings; this determines what will
439         * be written out. 
440         */
441        public EnumIoMapped(@Nonnull Class<T> clazz, @Nonnull Map<String, T> mapToEnum, @Nonnull Map<T, String> mapFromEnum) {
442            this.clazz = clazz;
443            
444            this.mapToEnum = mapToEnum;
445            
446            this.mapFromEnum = mapFromEnum;
447        }
448
449        /**
450         * @param clazz enum class.
451         * @param mapToEnum Substitutes an explicit mapping
452         * for mapping from Strings to enums; this could allow e.g.
453         * accepting both name and number versions. Multiple entries
454         * are OK: this can map both "1" and "Foo" to Foo for past-schema support.
455         * The mapping from enums to Strings uses the enum names.
456         */
457        public EnumIoMapped(@Nonnull Class<T> clazz, @Nonnull Map<String, T> mapToEnum) {
458            this.clazz = clazz;
459            
460            this.mapToEnum = mapToEnum;
461            
462            this.mapFromEnum = new HashMap<>();
463            for (T t : clazz.getEnumConstants() ) {
464                this.mapFromEnum.put(t, t.name());
465            }
466        }
467
468        final Class<T> clazz;
469        final Map<T, String> mapFromEnum;
470        final Map<String, T> mapToEnum;
471        
472        /** {@inheritDoc} */
473        @Override
474        @Nonnull
475        public String outputFromEnum(@Nonnull T e) {
476            String retval = mapFromEnum.get(e);
477            log.trace("from {} make String {} for {}", e, retval, clazz);
478            return retval;
479        }
480        
481        /** {@inheritDoc} */
482        @Override
483        @Nonnull
484        public T inputFromString(@CheckForNull String s) {
485            if (s == null) {
486                log.error("from String null get {} for {}", clazz.getEnumConstants()[0].name(), clazz);
487                return clazz.getEnumConstants()[0];
488            }
489            
490            try {
491                T retval = mapToEnum.get(s);
492                if (retval == null) {
493                    log.error("from String {} get {} for {}", s, clazz.getEnumConstants()[0].name(), clazz);
494                    return clazz.getEnumConstants()[0];
495                } else {
496                    log.trace("from String {} get {} for {}", s, retval, clazz);
497                    return retval;
498                }
499            } catch (RuntimeException e) {
500                log.error("from String {} get {} for {}", s, clazz.getEnumConstants()[0].name(), clazz, e);
501                return clazz.getEnumConstants()[0];
502            }
503        }
504    }
505
506    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AbstractXmlAdapter.class);
507}