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
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
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
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
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
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
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
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
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
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 }