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