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