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