View Javadoc

1   /***
2    *  Copyright 2003-2010 Terracotta, Inc.
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 java.lang.reflect.Constructor;
20  import java.lang.reflect.InvocationTargetException;
21  import java.lang.reflect.Method;
22  import java.lang.reflect.Modifier;
23  import java.util.ArrayList;
24  import java.util.HashSet;
25  import java.util.Set;
26  
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  import org.xml.sax.Attributes;
30  import org.xml.sax.Locator;
31  import org.xml.sax.SAXException;
32  import org.xml.sax.helpers.DefaultHandler;
33  
34  /***
35   * A SAX handler that configures a bean.
36   *
37   * @version $Id: BeanHandler.html 13146 2011-08-01 17:12:39Z oletizi $
38   * @author Adam Murdoch
39   * @author Greg Luck
40   */
41  final class BeanHandler extends DefaultHandler {
42  
43      private static final Logger LOG = LoggerFactory.getLogger(BeanHandler.class.getName());
44      private final Object bean;
45      private ElementInfo element;
46      private Locator locator;
47  
48      // State for extracting a subtree
49      private String subtreeMatchingQname;
50      private StringBuilder subtreeText;
51      private Method subtreeMethod;
52  
53      /***
54       * Constructor.
55       */
56      public BeanHandler(final Object bean) {
57          this.bean = bean;
58      }
59  
60      /***
61       * Receive a Locator object for document events.
62       */
63      @Override
64      public final void setDocumentLocator(Locator locator) {
65          this.locator = locator;
66      }
67  
68      private String getTagPart(String qName) {
69          String[] parts = qName.split(":");
70          return parts[parts.length - 1];
71      }
72  
73      /***
74       * Receive notification of the start of an element.
75       */
76      @Override
77      public final void startElement(final String uri,
78                               final String localName,
79                               final String qName,
80                               final Attributes attributes)
81              throws SAXException {
82  
83          boolean subtreeAppend = extractingSubtree();
84          if (extractingSubtree() || startExtractingSubtree(getTagPart(qName))) {
85              if (subtreeAppend) {
86                  appendToSubtree("<" + qName);
87              }
88  
89              for (int i = 0; i < attributes.getLength(); i++) {
90                  final String attrName = attributes.getQName(i);
91                  final String attrValue = attributes.getValue(i);
92                  if (subtreeAppend) {
93                      appendToSubtree(" " + attrName + "=\"" + attrValue + "\"");
94                  }
95              }
96  
97              if (subtreeAppend) {
98                  appendToSubtree(">");
99              }
100             element = new ElementInfo(element, qName, bean);
101         } else {
102             if (element == null) {
103                 element = new ElementInfo(qName, bean);
104             } else {
105                 final Object child = createChild(element, qName);
106                 element = new ElementInfo(element, qName, child);
107             }
108 
109             // Set the attributes
110             for (int i = 0; i < attributes.getLength(); i++) {
111                 final String attrName = attributes.getQName(i);
112                 final String attrValue = attributes.getValue(i);
113                 setAttribute(element, attrName, attrValue);
114             }
115         }
116     }
117 
118     /***
119      * Receive notification of the end of an element.
120      */
121     @Override
122     public final void endElement(final String uri,
123                            final String localName,
124                            final String qName)
125             throws SAXException {
126 
127         if (element.parent != null) {
128             if (extractingSubtree()) {
129                 if (endsSubtree(getTagPart(qName))) {
130                     endSubtree();
131                 } else {
132                     appendToSubtree("</" + qName + ">");
133                 }
134             } else {
135                 addChild(element.parent.bean, element.bean, qName);
136             }
137         }
138         element = element.parent;
139     }
140 
141     /***
142      * Receive notification of character data within an element - only used currently when
143      * extracting an xml subtree
144      */
145     @Override
146     public void characters(char[] ch, int start, int length)
147             throws SAXException {
148 
149         if (extractingSubtree()) {
150             appendToSubtree(ch, start, length);
151         }
152     }
153 
154     /***
155      * Creates a child element of an object.
156      */
157     private Object createChild(final ElementInfo parent, final String name)
158             throws SAXException {
159 
160         try {
161             // Look for a create<name> method
162             final Class parentClass = parent.bean.getClass();
163             Method method = findCreateMethod(parentClass, name);
164             if (method != null) {
165                 return method.invoke(parent.bean, new Object[] {});
166             }
167 
168             // Look for an add<name> method
169             method = findSetMethod(parentClass, "add", name);
170             if (method != null) {
171                 return createInstance(parent.bean, method.getParameterTypes()[0]);
172             }
173         } catch (final Exception e) {
174             throw new SAXException(getLocation() + ": Could not create nested element <" + name + ">.", e);
175         }
176 
177         throw new SAXException(getLocation()
178                 + ": Element <"
179                 + parent.elementName
180                 + "> does not allow nested <"
181                 + name
182                 + "> elements.");
183     }
184 
185     /***
186      * Creates a child object.
187      */
188     private static Object createInstance(Object parent, Class childClass)
189             throws Exception {
190         final Constructor[] constructors = childClass.getDeclaredConstructors();
191         ArrayList candidates = new ArrayList();
192         for (final Constructor constructor : constructors) {
193             final Class[] params = constructor.getParameterTypes();
194             if (params.length == 0) {
195                 candidates.add(constructor);
196             } else if (params.length == 1 && params[0].isInstance(parent)) {
197                 candidates.add(constructor);
198             }
199         }
200         switch (candidates.size()) {
201             case 0:
202                 throw new Exception("No constructor for class " + childClass.getName());
203             case 1:
204                 break;
205             default:
206                 throw new Exception("Multiple constructors for class " + childClass.getName());
207         }
208 
209         final Constructor constructor = (Constructor) candidates.remove(0);
210         constructor.setAccessible(true);
211         if (constructor.getParameterTypes().length == 0) {
212             return constructor.newInstance(new Object[] {});
213         } else {
214             return constructor.newInstance(new Object[]{parent});
215         }
216     }
217 
218     /***
219      * Finds a creator method.
220      */
221     private static Method findCreateMethod(Class objClass, String name) {
222         final String methodName = makeMethodName("create", name);
223         final Method[] methods = objClass.getMethods();
224         for (final Method method : methods) {
225             if (!method.getName().equals(methodName)) {
226                 continue;
227             }
228             if (Modifier.isStatic(method.getModifiers())) {
229                 continue;
230             }
231             if (method.getParameterTypes().length != 0) {
232                 continue;
233             }
234             if (method.getReturnType().isPrimitive() || method.getReturnType().isArray()) {
235                 continue;
236             }
237             return method;
238         }
239 
240         return null;
241     }
242 
243     /***
244      * Builds a method name from an element or attribute name.
245      */
246     private static String makeMethodName(final String prefix, final String name) {
247         String rawName = prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
248 
249         // Remove "-" in element name
250         return rawName.replace("-", "");
251     }
252 
253     /***
254      * Sets an attribute.
255      */
256     private void setAttribute(final ElementInfo element,
257                               final String attrName,
258                               final String attrValue)
259             throws SAXException {
260         try {
261             // Look for a set<name> method
262             final Class objClass = element.bean.getClass();
263             final Method method = chooseSetMethod(objClass, "set", attrName, String.class);
264             if (method != null) {
265                 final Object realValue = convert(attrName, method.getParameterTypes()[0], attrValue);
266                 method.invoke(element.bean, new Object[]{realValue});
267                 return;
268             } else {
269                 //allow references to an XML schema but do not use it
270                 if (element.elementName.equals("ehcache")) {
271                     LOG.debug("Ignoring ehcache attribute {}", attrName);
272                     return;
273                 }
274             }
275         } catch (final InvocationTargetException e) {
276             throw new SAXException(getLocation() + ": Could not set attribute \"" + attrName + "\"."
277                 + ". Message was: " + e.getTargetException());
278         } catch (final Exception e) {
279             throw new SAXException(getLocation() + ": Could not set attribute \"" + attrName + "\" - " + e.getMessage());
280         }
281 
282         throw new SAXException(getLocation()
283                 + ": Element <"
284                 + element.elementName
285                 + "> does not allow attribute \""
286                 + attrName
287                 + "\".");
288     }
289 
290     /***
291      * Converts a string to an object of a particular class.
292      * @param attrName Name of attribute
293      */
294     private static Object convert(String attributeName, final Class toClass, final String value)
295             throws Exception {
296         if (value == null) {
297             return null;
298         }
299         if (toClass.isInstance(value)) {
300             return value;
301         }
302         if (toClass == Long.class || toClass == Long.TYPE) {
303             return Long.decode(value);
304         }
305         if (toClass == Integer.class || toClass == Integer.TYPE) {
306             return Integer.decode(value);
307         }
308         if (toClass == Boolean.class || toClass == Boolean.TYPE) {
309             if ("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value)) {
310                 return Boolean.valueOf(value);
311             } else {
312                 throw new InvalidConfigurationException("Invalid value specified for attribute '" + attributeName
313                         + "', please use 'true' or 'false' instead of '" + value + "'");
314             }
315         }
316         throw new Exception("Cannot convert attribute value to class " + toClass.getName());
317     }
318 
319     private Method chooseSetMethod(final Class objClass, final String prefix, final String name, final Class preferredParameterType)
320             throws Exception {
321         final String methodName = makeMethodName(prefix, name);
322         final Method[] methods = objClass.getMethods();
323         Set<Method> candidates = new HashSet<Method>();
324         for (final Method method : methods) {
325             if (!method.getName().equals(methodName)) {
326                 continue;
327             }
328             if (Modifier.isStatic(method.getModifiers())) {
329                 continue;
330             }
331             if (method.getParameterTypes().length != 1) {
332                 continue;
333             }
334             if (!method.getReturnType().equals(Void.TYPE)) {
335                 continue;
336             }
337             candidates.add(method);
338         }
339         if (candidates.size() == 0) {
340             return null;
341         } else if (candidates.size() == 1) {
342             return candidates.iterator().next();
343         } else {
344             for (Method m : candidates) {
345                 if (m.getParameterTypes()[0].equals(preferredParameterType)) {
346                     return m;
347                 }
348             }
349             throw new Exception("Multiple " + methodName + "() methods found in class " + objClass.getName()
350                     + ", but not one with preferred parameter type - " + preferredParameterType.getName());
351         }
352     }
353 
354     /***
355      * Finds a setter method.
356      */
357     private Method findSetMethod(final Class objClass,
358                                  final String prefix,
359                                  final String name)
360             throws Exception {
361         final String methodName = makeMethodName(prefix, name);
362         final Method[] methods = objClass.getMethods();
363         Method candidate = null;
364         for (final Method method : methods) {
365             if (!method.getName().equals(methodName)) {
366                 continue;
367             }
368             if (Modifier.isStatic(method.getModifiers())) {
369                 continue;
370             }
371             if (method.getParameterTypes().length != 1) {
372                 continue;
373             }
374             if (!method.getReturnType().equals(Void.TYPE)) {
375                 continue;
376             }
377             if (candidate != null) {
378                 throw new Exception("Multiple " + methodName + "() methods in class " + objClass.getName() + ".");
379             }
380             candidate = method;
381         }
382 
383         return candidate;
384     }
385 
386     /***
387      * Attaches a child element to its parent.
388      */
389     private void addChild(final Object parent,
390                           final Object child,
391                           final String name)
392             throws SAXException {
393         try {
394             // Look for an add<name> method on the parent
395             final Method method = findSetMethod(parent.getClass(), "add", name);
396             if (method != null) {
397                 method.invoke(parent, new Object[]{child});
398             }
399         } catch (final InvocationTargetException e) {
400             final SAXException exc = new SAXException(getLocation() + ": Could not finish element <" + name + ">." +
401                     " Message was: " + e.getTargetException());
402             throw exc;
403         } catch (final Exception e) {
404             throw new SAXException(getLocation() + ": Could not finish element <" + name + ">.");
405         }
406     }
407 
408     /***
409      * Formats the current document location.
410      */
411     private String getLocation() {
412         return locator.getSystemId() + ':' + locator.getLineNumber();
413     }
414 
415     /***
416      * Determine whether we should start extracting a subtree, based on
417      * whether there is an extract method for this tag in the parent bean.
418      */
419     private boolean startExtractingSubtree(String name) throws SAXException {
420         // if need to start extracting, stow the name
421         if (element == null || element.bean == null) {
422             return false;
423         }
424 
425         try {
426             final Method method = findSetMethod(element.bean.getClass(), "extract", name);
427             if (method != null) {
428                 subtreeMatchingQname = name;
429                 subtreeText = new StringBuilder();
430                 subtreeMethod = method;
431                 return true;
432             } else {
433                 return false;
434             }
435 
436         } catch (Exception e) {
437             throw new SAXException(getLocation() + ": Error checking for extract method on <" + name + ">.");
438         }
439     }
440 
441     private boolean extractingSubtree() {
442         return this.subtreeMatchingQname != null;
443     }
444 
445     /***
446      * Append to the current extracted subtree text
447      */
448     private void appendToSubtree(String text) {
449         subtreeText.append(text);
450     }
451 
452     /***
453      * Append to the current extracted subtree text
454      */
455     private void appendToSubtree(char[] text, int start, int length) {
456         subtreeText.append(text, start, length);
457     }
458 
459     /***
460      * Determine whether the current endName tag ends the subtree matching
461      */
462     private boolean endsSubtree(String endName) {
463         return this.subtreeMatchingQname != null && this.subtreeMatchingQname.equals(endName);
464     }
465 
466     private void endSubtree() throws SAXException {
467         try {
468             subtreeMethod.invoke(element.parent.bean, new Object[]{subtreeText.toString()});
469         } catch (InvocationTargetException e) {
470             throw new SAXException(getLocation() + ": Could not set extracted subtree \"" + subtreeMatchingQname + "\"."
471                 + " Message was: " + e.getTargetException());
472         } catch (Exception e) {
473             throw new SAXException(getLocation() + ": Could not set extracted subtree \"" + subtreeMatchingQname + "\"."
474                 + " Message was: " + e.getMessage());
475         }
476 
477         subtreeMatchingQname = null;
478         subtreeMethod = null;
479         subtreeText = null;
480     }
481 
482     /***
483      * Element info class
484      */
485     private static final class ElementInfo {
486         private final ElementInfo parent;
487         private final String elementName;
488         private final Object bean;
489 
490         public ElementInfo(final String elementName, final Object bean) {
491             parent = null;
492             this.elementName = elementName;
493             this.bean = bean;
494         }
495 
496         public ElementInfo(final ElementInfo parent, final String elementName, final Object bean) {
497             this.parent = parent;
498             this.elementName = elementName;
499             this.bean = bean;
500         }
501     }
502 }