View Javadoc

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