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