View Javadoc

1   /**
2    *  Copyright 2003-2006 Greg Luck
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   */
16  
17  package net.sf.ehcache.config;
18  
19  import org.xml.sax.Attributes;
20  import org.xml.sax.Locator;
21  import org.xml.sax.SAXException;
22  import org.xml.sax.helpers.DefaultHandler;
23  
24  import java.lang.reflect.Constructor;
25  import java.lang.reflect.InvocationTargetException;
26  import java.lang.reflect.Method;
27  import java.lang.reflect.Modifier;
28  import java.util.ArrayList;
29  
30  /**
31   * A SAX handler that configures a bean.
32   *
33   * @version $Id: BeanHandler.java 53 2006-04-25 08:56:21Z gregluck $
34   * @author Adam Murdoch
35   * @author Greg Luck
36   */
37  final class BeanHandler extends DefaultHandler {
38      private final Object bean;
39      private ElementInfo element;
40      private Locator locator;
41  
42  
43      /**
44       * Constructor.
45       */
46      public BeanHandler(final Object bean) {
47          this.bean = bean;
48      }
49  
50      /**
51       * Receive a Locator object for document events.
52       */
53      public final void setDocumentLocator(Locator locator) {
54          this.locator = locator;
55      }
56  
57      /**
58       * Receive notification of the start of an element.
59       */
60      public final void startElement(final String uri,
61                               final String localName,
62                               final String qName,
63                               final Attributes attributes)
64              throws SAXException {
65          // Create the child object
66          if (element == null) {
67              element = new ElementInfo(qName, bean);
68          } else {
69              final Object child = createChild(element, qName);
70              element = new ElementInfo(element, qName, child);
71          }
72  
73          // Set the attributes
74          for (int i = 0; i < attributes.getLength(); i++) {
75              final String attrName = attributes.getQName(i);
76              final String attrValue = attributes.getValue(i);
77              setAttribute(element, attrName, attrValue);
78          }
79      }
80  
81      /**
82       * Receive notification of the end of an element.
83       */
84      public final void endElement(final String uri,
85                             final String localName,
86                             final String qName)
87              throws SAXException {
88          if (element.parent != null) {
89              addChild(element.parent.bean, element.bean, qName);
90          }
91          element = element.parent;
92      }
93  
94      /**
95       * Creates a child element of an object.
96       */
97      private Object createChild(final ElementInfo parent, final String name)
98              throws SAXException {
99  
100         try {
101             // Look for a create<name> method
102             final Class parentClass = parent.bean.getClass();
103             Method method = findCreateMethod(parentClass, name);
104             if (method != null) {
105                 return method.invoke(parent.bean, new Object[] {});
106             }
107 
108             // Look for an add<name> method
109             method = findSetMethod(parentClass, "add", name);
110             if (method != null) {
111                 return createInstance(parent.bean, method.getParameterTypes()[0]);
112             }
113         } catch (final Exception e) {
114             throw new SAXException(getLocation() + ": Could not create nested element <" + name + ">.");
115         }
116 
117         throw new SAXException(getLocation()
118                 + ": Element <"
119                 + parent.elementName
120                 + "> does not allow nested <"
121                 + name
122                 + "> elements.");
123     }
124 
125     /**
126      * Creates a child object.
127      */
128     private static Object createInstance(Object parent, Class childClass)
129             throws Exception {
130         final Constructor[] constructors = childClass.getConstructors();
131         ArrayList candidates = new ArrayList();
132         for (int i = 0; i < constructors.length; i++) {
133             final Constructor constructor = constructors[i];
134             final Class[] params = constructor.getParameterTypes();
135             if (params.length == 0) {
136                 candidates.add(constructor);
137             } else if (params.length == 1 && params[0].isInstance(parent)) {
138                 candidates.add(constructor);
139             }
140         }
141         switch (candidates.size()) {
142             case 0:
143                 throw new Exception("No constructor for class " + childClass.getName());
144             case 1:
145                 break;
146             default:
147                 throw new Exception("Multiple constructors for class " + childClass.getName());
148         }
149 
150         final Constructor constructor = (Constructor) candidates.remove(0);
151         if (constructor.getParameterTypes().length == 0) {
152             return constructor.newInstance(new Object[] {});
153         } else {
154             return constructor.newInstance(new Object[]{parent});
155         }
156     }
157 
158     /**
159      * Finds a creator method.
160      */
161     private static Method findCreateMethod(Class objClass, String name) {
162         final String methodName = makeMethodName("create", name);
163         final Method[] methods = objClass.getMethods();
164         for (int i = 0; i < methods.length; i++) {
165             final Method method = methods[i];
166             if (!method.getName().equals(methodName)) {
167                 continue;
168             }
169             if (Modifier.isStatic(method.getModifiers())) {
170                 continue;
171             }
172             if (method.getParameterTypes().length != 0) {
173                 continue;
174             }
175             if (method.getReturnType().isPrimitive() || method.getReturnType().isArray()) {
176                 continue;
177             }
178             return method;
179         }
180 
181         return null;
182     }
183 
184     /**
185      * Builds a method name from an element or attribute name.
186      */
187     private static String makeMethodName(final String prefix, final String name) {
188         return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
189     }
190 
191     /**
192      * Sets an attribute.
193      */
194     private void setAttribute(final ElementInfo element,
195                               final String attrName,
196                               final String attrValue)
197             throws SAXException {
198         try {
199             // Look for a set<name> method
200             final Class objClass = element.bean.getClass();
201             final Method method = findSetMethod(objClass, "set", attrName);
202             if (method != null) {
203                 final Object realValue = convert(method.getParameterTypes()[0], attrValue);
204                 method.invoke(element.bean, new Object[]{realValue});
205                 return;
206             }
207         } catch (final InvocationTargetException e) {
208             throw new SAXException(getLocation() + ": Could not set attribute \"" + attrName + "\"."
209                 + ". Message was: " + e.getTargetException());
210         } catch (final Exception e) {
211             throw new SAXException(getLocation() + ": Could not set attribute \"" + attrName + "\".");
212         }
213 
214         throw new SAXException(getLocation()
215                 + ": Element <"
216                 + element.elementName
217                 + "> does not allow attribute \""
218                 + attrName
219                 + "\".");
220     }
221 
222     /**
223      * Converts a string to an object of a particular class.
224      */
225     private static Object convert(final Class toClass, final String value)
226             throws Exception {
227         if (value == null) {
228             return null;
229         }
230         if (toClass.isInstance(value)) {
231             return value;
232         }
233         if (toClass == Long.class || toClass == Long.TYPE) {
234             return Long.decode(value);
235         }
236         if (toClass == Integer.class || toClass == Integer.TYPE) {
237             return Integer.decode(value);
238         }
239         if (toClass == Boolean.class || toClass == Boolean.TYPE) {
240             return Boolean.valueOf(value);
241         }
242         throw new Exception("Cannot convert attribute value to class " + toClass.getName());
243     }
244 
245     /**
246      * Finds a setter method.
247      */
248     private Method findSetMethod(final Class objClass,
249                                  final String prefix,
250                                  final String name)
251             throws Exception {
252         final String methodName = makeMethodName(prefix, name);
253         final Method[] methods = objClass.getMethods();
254         Method candidate = null;
255         for (int i = 0; i < methods.length; i++) {
256             final Method method = methods[i];
257             if (!method.getName().equals(methodName)) {
258                 continue;
259             }
260             if (Modifier.isStatic(method.getModifiers())) {
261                 continue;
262             }
263             if (method.getParameterTypes().length != 1) {
264                 continue;
265             }
266             if (!method.getReturnType().equals(Void.TYPE)) {
267                 continue;
268             }
269             if (candidate != null) {
270                 throw new Exception("Multiple " + methodName + "() methods in class " + objClass.getName() + ".");
271             }
272             candidate = method;
273         }
274 
275         return candidate;
276     }
277 
278     /**
279      * Attaches a child element to its parent.
280      */
281     private void addChild(final Object parent,
282                           final Object child,
283                           final String name)
284             throws SAXException {
285         try {
286             // Look for an add<name> method on the parent
287             final Method method = findSetMethod(parent.getClass(), "add", name);
288             if (method != null) {
289                 method.invoke(parent, new Object[]{child});
290             }
291         } catch (final InvocationTargetException e) {
292             final SAXException exc = new SAXException(getLocation() + ": Could not finish element <" + name + ">." +
293                     " Message was: " + e.getTargetException());
294             throw exc;
295         } catch (final Exception e) {
296             throw new SAXException(getLocation() + ": Could not finish element <" + name + ">.");
297         }
298     }
299 
300     /**
301      * Formats the current document location.
302      */
303     private String getLocation() {
304         return locator.getSystemId() + ':' + locator.getLineNumber();
305     }
306 
307     /**
308      * Element info class
309      */
310     private static final class ElementInfo {
311         private final ElementInfo parent;
312         private final String elementName;
313         private final Object bean;
314 
315         public ElementInfo(final String elementName, final Object bean) {
316             parent = null;
317             this.elementName = elementName;
318             this.bean = bean;
319         }
320 
321         public ElementInfo(final ElementInfo parent, final String elementName, final Object bean) {
322             this.parent = parent;
323             this.elementName = elementName;
324             this.bean = bean;
325         }
326     }
327 }