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