1   /**
2    * Distribution License:
3    * JSword is free software; you can redistribute it and/or modify it under
4    * the terms of the GNU Lesser General Public License, version 2.1 or later
5    * as published by the Free Software Foundation. This program is distributed
6    * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
7    * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8    * See the GNU Lesser General Public License for more details.
9    *
10   * The License is available on the internet at:
11   *       http://www.gnu.org/copyleft/lgpl.html
12   * or by writing to:
13   *      Free Software Foundation, Inc.
14   *      59 Temple Place - Suite 330
15   *      Boston, MA 02111-1307, USA
16   *
17   * © CrossWire Bible Society, 2005 - 2016
18   *
19   */
20  package org.crosswire.common.config;
21  
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.util.MissingResourceException;
25  import java.util.ResourceBundle;
26  
27  import org.crosswire.common.util.ClassUtil;
28  import org.crosswire.common.util.StringUtil;
29  import org.crosswire.jsword.JSOtherMsg;
30  import org.jdom2.Element;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  /**
35   * A helper for when we need to be a choice created dynamically.
36   * 
37   * @see gnu.lgpl.License The GNU Lesser General Public License for details.
38   * @author Joe Walker
39   * @author DM Smith
40   */
41  public abstract class AbstractReflectedChoice implements Choice {
42      /*
43       * (non-Javadoc)
44       * 
45       * @see org.crosswire.common.config.Choice#init(org.jdom2.Element)
46       */
47      public void init(Element option, ResourceBundle configResources) throws StartupException {
48          assert configResources != null;
49  
50          key = option.getAttributeValue("key");
51  
52          // Hidden is an optional field so it is ok for the resource to be
53          // missing.
54          try {
55              String hiddenState = configResources.getString(key + ".hidden");
56              hidden = Boolean.valueOf(hiddenState).booleanValue();
57          } catch (MissingResourceException e) {
58              hidden = false;
59          }
60  
61          // Ignore is an optional field so it is ok for the resource to be
62          // missing.
63          try {
64              String ignoreState = configResources.getString(key + ".ignore");
65              ignored = Boolean.valueOf(ignoreState).booleanValue();
66              if (ignored) {
67                  hidden = true;
68                  return;
69              }
70          } catch (MissingResourceException e) {
71              ignored = false;
72          }
73  
74          String helpText = configResources.getString(key + ".help");
75          assert helpText != null;
76          setHelpText(helpText);
77  
78          // OPTIMIZE(dms): This is poorly done (by me!)
79          String[] pathParts = StringUtil.split(key, '.');
80          StringBuilder parentKey = new StringBuilder();
81          StringBuilder path = new StringBuilder();
82          for (int i = 0; i < pathParts.length; i++) {
83              if (i > 0) {
84                  parentKey.append('.');
85                  path.append('.');
86              }
87              parentKey.append(pathParts[i]);
88              String parent = configResources.getString(parentKey + ".name");
89              assert parent != null;
90              path.append(parent);
91          }
92          setFullPath(path.toString());
93  
94          external = Boolean.valueOf(option.getAttributeValue("external")).booleanValue();
95  
96          restart = Boolean.valueOf(option.getAttributeValue("restart")).booleanValue();
97  
98          type = option.getAttributeValue("type");
99  
100         // The important 3 things saying what we update and how we describe
101         // ourselves
102         Element introspector = option.getChild("introspect");
103         if (introspector == null) {
104             throw new StartupException(JSOtherMsg.lookupText("Missing {0} element in config.xml", "introspect"));
105         }
106 
107         String clazzname = introspector.getAttributeValue("class");
108         if (clazzname == null) {
109             throw new StartupException(JSOtherMsg.lookupText("Missing {0} element in config.xml", "class"));
110         }
111 
112         propertyname = introspector.getAttributeValue("property");
113         if (propertyname == null) {
114             throw new StartupException(JSOtherMsg.lookupText("Missing {0} element in config.xml", "property"));
115         }
116 
117         // log.debug("Looking up {}.set{}({} arg0)", clazzname, propertyname, getConvertionClass().getName());
118 
119         try {
120             clazz = ClassUtil.forName(clazzname);
121         } catch (ClassNotFoundException ex) {
122             throw new StartupException(JSOtherMsg.lookupText("Specified class not found: {0}", clazzname), ex);
123         }
124 
125         try {
126             setter = clazz.getMethod("set" + propertyname, getConversionClass());
127         } catch (NoSuchMethodException ex) {
128             throw new StartupException(JSOtherMsg.lookupText("Specified method not found {0}.set{1}({2} arg0)",
129                     clazz.getName(), propertyname, getConversionClass().getName()), ex
130             );
131         }
132 
133         try {
134             try {
135                 getter = clazz.getMethod("is" + propertyname, new Class[0]);
136             } catch (NoSuchMethodException e) {
137                 getter = clazz.getMethod("get" + propertyname, new Class[0]);
138             }
139         } catch (NoSuchMethodException ex) {
140             throw new StartupException(JSOtherMsg.lookupText("Specified method not found {0}.get{1}()", clazz.getName(), propertyname), ex);
141         }
142 
143         if (getter.getReturnType() != getConversionClass()) {
144             log.debug("Not using {} from {} because the return type of the getter is not {}", propertyname, clazz.getName(), getConversionClass().getName());
145             throw new StartupException(JSOtherMsg.lookupText("Mismatch of return types, found: {0} required: {1}", getter.getReturnType(), getConversionClass()));
146         }
147     }
148 
149     /*
150      * (non-Javadoc)
151      * 
152      * @see org.crosswire.common.config.Choice#getKey()
153      */
154     public String getKey() {
155         return key;
156     }
157 
158     /*
159      * (non-Javadoc)
160      * 
161      * @see org.crosswire.common.config.Choice#getType()
162      */
163     public String getType() {
164         return type;
165     }
166 
167     /**
168      * Convert from a reflection return value to a String for storage
169      * 
170      * @param orig the object to be converted to a string
171      * @return the marshaled representation of the object
172      */
173     public abstract String convertToString(Object orig);
174 
175     /**
176      * Convert from a stored string to an object to use with reflection
177      * @param orig the marshaled representation of the object
178      * @return the reconstituted object
179      */
180     public abstract Object convertToObject(String orig);
181 
182     /*
183      * (non-Javadoc)
184      * 
185      * @see org.crosswire.common.config.Choice#getFullPath()
186      */
187     public String getFullPath() {
188         return fullPath;
189     }
190 
191     /*
192      * (non-Javadoc)
193      * 
194      * @see org.crosswire.common.config.Choice#setFullPath(java.lang.String)
195      */
196     public void setFullPath(String newFullPath) {
197         fullPath = newFullPath;
198     }
199 
200     /*
201      * (non-Javadoc)
202      * 
203      * @see org.crosswire.common.config.Choice#getHelpText()
204      */
205     public String getHelpText() {
206         return helptext;
207     }
208 
209     /*
210      * (non-Javadoc)
211      * 
212      * @see org.crosswire.common.config.Choice#setHelpText(java.lang.String)
213      */
214     public void setHelpText(String helptext) {
215         this.helptext = helptext;
216     }
217 
218     /*
219      * (non-Javadoc)
220      * 
221      * @see org.crosswire.common.config.Choice#isSaveable()
222      */
223     public boolean isSaveable() {
224         return !external;
225     }
226 
227     /*
228      * (non-Javadoc)
229      * 
230      * @see org.crosswire.common.config.Choice#isHidden()
231      */
232     public boolean isHidden() {
233         return hidden;
234     }
235 
236     /*
237      * (non-Javadoc)
238      * 
239      * @see org.crosswire.common.config.Choice#isIgnored()
240      */
241     public boolean isIgnored() {
242         return ignored;
243     }
244 
245     /*
246      * (non-Javadoc)
247      * 
248      * @see org.crosswire.common.config.Choice#requiresRestart()
249      */
250     public boolean requiresRestart() {
251         return restart;
252     }
253 
254     /*
255      * (non-Javadoc)
256      * 
257      * @see org.crosswire.common.config.Choice#getString()
258      */
259     public String getString() {
260         try {
261             Object retval = getter.invoke(null, new Object[0]);
262             return convertToString(retval);
263         } catch (IllegalAccessException ex) {
264             log.error("Illegal access getting value from {}.{}", clazz.getName(), getter.getName(), ex);
265             return "";
266         } catch (InvocationTargetException ex) {
267             log.error("Failed to get value from {}.{}", clazz.getName(), getter.getName(), ex);
268             return "";
269         }
270     }
271 
272     /*
273      * (non-Javadoc)
274      * 
275      * @see org.crosswire.common.config.Choice#setString(java.lang.String)
276      */
277     public void setString(String value) throws ConfigException {
278         Exception ex = null;
279         try {
280             Object object = convertToObject(value);
281             if (object != null) {
282                 setter.invoke(null, object);
283             }
284         } catch (InvocationTargetException e) {
285             ex = e;
286         } catch (IllegalArgumentException e) {
287             ex = e;
288         } catch (IllegalAccessException e) {
289             ex = e;
290         } catch (NullPointerException e) {
291             ex = e;
292         }
293 
294         if (ex != null) {
295             log.info("Exception while attempting to execute: {}", setter.toString());
296 
297             // So we can't re-throw the original exception because it wasn't an
298             // Exception so we will have to re-throw the
299             // InvocationTargetException
300             throw new ConfigException(JSOtherMsg.lookupText("Failed to set option: {0}", setter), ex);
301         }
302     }
303 
304     /**
305      * The key of the option.
306      */
307     private String key;
308 
309     /**
310      * The type that we reflect to
311      */
312     private Class<? extends Object> clazz;
313 
314     /**
315      * The property that we call on the reflecting class
316      */
317     private String propertyname;
318 
319     /**
320      * The type (as specified in config.xml)
321      */
322     private String type;
323 
324     /**
325      * The method to call to get the value
326      */
327     private Method getter;
328 
329     /**
330      * The method to call to set the value
331      */
332     private Method setter;
333 
334     /**
335      * The help text (tooltip) for this item
336      */
337     private String helptext;
338 
339     /**
340      * The full path of this item
341      */
342     private String fullPath;
343 
344     /**
345      * Whether this choice should be visible or hidden
346      */
347     private boolean hidden;
348 
349     /**
350      * Whether this choice should be ignored altogether.
351      */
352     private boolean ignored;
353 
354     /**
355      * Whether this choice is managed externally, via setXXX and getXXX.
356      */
357     private boolean external;
358 
359     /**
360      * Whether this choice is requires a restart to be seen.
361      */
362     private boolean restart;
363 
364     /**
365      * The log stream
366      */
367     private static final Logger log = LoggerFactory.getLogger(AbstractReflectedChoice.class);
368 }
369