001 /**
002 *
003 * Licensed to the Apache Software Foundation (ASF) under one or more
004 * contributor license agreements. See the NOTICE file distributed with
005 * this work for additional information regarding copyright ownership.
006 * The ASF licenses this file to You under the Apache License, Version 2.0
007 * (the "License"); you may not use this file except in compliance with
008 * the License. You may obtain a copy of the License at
009 *
010 * http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018 package org.apache.camel.spring.xml;
019
020 import org.apache.camel.Expression;
021 import org.apache.camel.builder.Fluent;
022 import org.apache.camel.builder.FluentArg;
023 import org.apache.camel.builder.RouteBuilder;
024 import org.apache.camel.builder.ValueBuilder;
025 import org.springframework.beans.SimpleTypeConverter;
026 import org.springframework.beans.factory.config.RuntimeBeanReference;
027 import org.springframework.beans.factory.support.AbstractBeanDefinition;
028 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
029 import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
030 import org.springframework.beans.factory.xml.ParserContext;
031 import org.springframework.util.StringUtils;
032 import org.springframework.util.xml.DomUtils;
033 import org.w3c.dom.Attr;
034 import org.w3c.dom.Element;
035 import org.w3c.dom.NamedNodeMap;
036 import org.w3c.dom.Node;
037 import org.w3c.dom.NodeList;
038
039 import java.lang.annotation.Annotation;
040 import java.lang.reflect.Method;
041 import java.util.ArrayList;
042 import java.util.Collections;
043 import java.util.Comparator;
044 import java.util.HashMap;
045 import java.util.HashSet;
046 import java.util.LinkedHashMap;
047 import java.util.List;
048 import java.util.Map;
049 import java.util.Set;
050
051 public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser {
052 private final CamelNamespaceHandler namespaceHandler;
053 private int counter;
054
055 public CamelBeanDefinitionParser(CamelNamespaceHandler namespaceHandler) {
056 this.namespaceHandler = namespaceHandler;
057 }
058
059 protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
060 BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactoryBean.class);
061
062 List childElements = DomUtils.getChildElementsByTagName(element, "route");
063 ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size());
064
065 if (childElements != null && childElements.size() > 0) {
066 for (int i = 0; i < childElements.size(); ++i) {
067 Element routeElement = (Element) childElements.get(i);
068
069 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
070 Class type = parseBuilderElement(parserContext, routeElement, RouteBuilder.class, actions);
071 BuilderStatement statement = new BuilderStatement();
072 statement.setReturnType(type);
073 statement.setActions(actions);
074 routes.add(statement);
075 }
076 }
077
078 factory.addPropertyValue("routes", routes);
079 return factory.getBeanDefinition();
080 }
081
082 /**
083 * Use reflection to figure out what is the valid next element.
084 */
085 private Class parseBuilderElement(ParserContext parserContext, Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) {
086 Class currentBuilder = builder;
087 NodeList childElements = element.getChildNodes();
088 Element previousElement = null;
089 for (int i = 0; i < childElements.getLength(); ++i) {
090 Node node = childElements.item(i);
091 if (node.getNodeType() == Node.ELEMENT_NODE) {
092 currentBuilder = parseAction(parserContext, currentBuilder, actions, (Element) node, previousElement);
093 previousElement = (Element) node;
094 BuilderAction action = actions.get(actions.size() - 1);
095
096 if (action.getMethodInfo().methodAnnotation.nestedActions()) {
097 currentBuilder = parseBuilderElement(parserContext, (Element) node, currentBuilder, actions);
098 }
099 else {
100 // Make sure the there are no child elements.
101 if (hasChildElements(node)) {
102 throw new IllegalArgumentException("The element " + node.getLocalName() + " should not have any child elements.");
103 }
104 }
105 }
106 }
107
108 // Add the builder actions that are annotated with @Fluent(callOnElementEnd=true)
109 if (currentBuilder != null) {
110 Method[] methods = currentBuilder.getMethods();
111 for (int i = 0; i < methods.length; i++) {
112 Method method = methods[i];
113 Fluent annotation = method.getAnnotation(Fluent.class);
114 if (annotation != null && annotation.callOnElementEnd()) {
115
116 if (method.getParameterTypes().length > 0) {
117 throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): " + method);
118 }
119
120 MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
121 actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
122 currentBuilder = method.getReturnType();
123 }
124 }
125 }
126 return currentBuilder;
127 }
128
129 private boolean hasChildElements(Node node) {
130 NodeList nl = node.getChildNodes();
131 for (int j = 0; j < nl.getLength(); ++j) {
132 if (nl.item(j).getNodeType() == Node.ELEMENT_NODE) {
133 return true;
134 }
135 }
136 return false;
137 }
138
139 private Class parseAction(ParserContext parserContext, Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) {
140
141 String actionName = element.getLocalName();
142
143 // Get a list of method names that match the action.
144 ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName());
145 if (methods.isEmpty()) {
146 throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
147 }
148
149 // Pick the best method out of the list. Sort by argument length. Pick
150 // first longest match.
151 Collections.sort(methods, new Comparator<MethodInfo>() {
152 public int compare(MethodInfo m1, MethodInfo m2) {
153 return m1.method.getParameterTypes().length - m2.method.getParameterTypes().length;
154 }
155 });
156
157 // Build the possible list of arguments from the attributes and child
158 // elements
159 HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element);
160 HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element);
161
162 // Find the first method that we can supply arguments for.
163 MethodInfo match = null;
164 match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet());
165 if (match == null) {
166 throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
167 }
168
169 // lets convert any references
170 Set<Map.Entry<String, Object>> attributeEntries = attributeArguments.entrySet();
171 for (Map.Entry<String, Object> entry : attributeEntries) {
172 String name = entry.getKey();
173 FluentArg arg = match.parameterAnnotations.get(name);
174 if (arg != null && (arg.reference() || name.equals("ref"))) {
175 Object value = entry.getValue();
176 if (value instanceof String) {
177 entry.setValue(new RuntimeBeanReference(value.toString()));
178 }
179 }
180 }
181
182 // Move element arguments into the attributeArguments map if needed.
183 Set<String> parameterNames = new HashSet<String>(match.parameters.keySet());
184 parameterNames.removeAll(attributeArguments.keySet());
185 for (String key : parameterNames) {
186 ArrayList<Element> elements = elementArguments.get(key);
187 if (elements == null) {
188 elements = getFirstChildElements(element);
189 }
190 Class clazz = match.parameters.get(key);
191 Object value = convertTo(parserContext, elements, clazz);
192 attributeArguments.put(key, value);
193 for (Element el : elements) {
194 // remove the argument nodes so that they don't get interpreted as
195 // actions.
196 el.getParentNode().removeChild(el);
197 }
198 }
199
200 actions.add(new BuilderAction(match, attributeArguments));
201 return match.method.getReturnType();
202 }
203
204 private ArrayList<Element> getFirstChildElements(Element element) {
205 ArrayList<Element> answer = new ArrayList<Element>();
206 NodeList list = element.getChildNodes();
207 for (int i = 0, size = list.getLength(); i < size; i++) {
208 Node node = list.item(i);
209 if (node instanceof Element) {
210 answer.add((Element) node);
211 break;
212 }
213 }
214 return answer;
215 }
216
217 private Object convertTo(ParserContext parserContext, ArrayList<Element> elements, Class clazz) {
218
219 if (clazz.isArray() || elements.size() > 1) {
220 List list = new ArrayList();
221 for (int i = 0; i < elements.size(); i++) {
222 ArrayList<Element> e = new ArrayList<Element>(1);
223 e.add(elements.get(i));
224 Object value = convertTo(parserContext, e, clazz.getComponentType());
225
226 list.add(value);
227 }
228 return list;
229 /*
230 Object array = Array.newInstance(clazz.getComponentType(), elements.size());
231 for( int i=0; i < elements.size(); i ++ ) {
232 ArrayList<Element> e = new ArrayList<Element>(1);
233 e.add(elements.get(i));
234 Object value = convertTo(parserContext, e, clazz.getComponentType());
235
236 Array.set(array, i, value);
237 }
238 return array;
239 */
240 }
241 else {
242
243 Element element = elements.get(0);
244 String ref = element.getAttribute("ref");
245 if (StringUtils.hasText(ref)) {
246 return new RuntimeBeanReference(ref);
247 }
248
249 // Use a builder to create the value..
250 if (hasChildElements(element)) {
251
252 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
253 Class type = parseBuilderElement(parserContext, element, RouteBuilder.class, actions);
254
255 if (type == ValueBuilder.class && clazz == Expression.class) {
256 Method method;
257 try {
258 method = ValueBuilder.class.getMethod("getExpression", new Class[]{});
259 }
260 catch (Throwable e) {
261 throw new RuntimeException(ValueBuilder.class.getName() + " does not have the getExpression() method.");
262 }
263 MethodInfo methodInfo = new MethodInfo(method, null, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
264 actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
265 type = Expression.class;
266 }
267
268 BuilderStatement statement = new BuilderStatement();
269 statement.setReturnType(type);
270 statement.setActions(actions);
271
272 if (!clazz.isAssignableFrom(statement.getReturnType())) {
273 throw new IllegalStateException("Builder does not produce object of expected type: " + clazz.getName() + ", it produced: " + statement.getReturnType());
274 }
275
276 return statement;
277 }
278 else {
279 // if we are on an element which has a custom parser, lets use that.
280 String name = element.getLocalName();
281 if (namespaceHandler.getParserElementNames().contains(name)) {
282 String id = createBeanId(name);
283 element.setAttribute("id", id);
284 namespaceHandler.parse(element, parserContext);
285 return new RuntimeBeanReference(id);
286 }
287
288 // Just use the text in the element as the value.
289 SimpleTypeConverter converter = new SimpleTypeConverter();
290 return converter.convertIfNecessary(element.getTextContent(), clazz);
291 }
292 }
293 }
294
295 protected synchronized String createBeanId(String name) {
296 return "_internal:camel:bean:" + name + (++counter);
297 }
298
299 private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) {
300 for (MethodInfo method : methods) {
301
302 // make sure all the given attribute parameters can be assigned via
303 // attributes
304 boolean miss = false;
305 for (String key : attributeNames) {
306 FluentArg arg = method.parameterAnnotations.get(key);
307 if (arg == null || !arg.attribute()) {
308 miss = true;
309 break;
310 }
311 }
312 if (miss) {
313 continue; // Keep looking...
314 }
315
316 Set<String> parameterNames = new HashSet<String>(method.parameters.keySet());
317 parameterNames.removeAll(attributeNames);
318
319 // Bingo we found a match.
320 if (parameterNames.isEmpty()) {
321 return method;
322 }
323
324 // We may still be able to match using elements as parameters.
325 /*
326 for (String key : elementNames) {
327 if (parameterNames.isEmpty()) {
328 break;
329 }
330 // We only want to use the first child elements as arguments,
331 // once we don't match, we can stop looking.
332 FluentArg arg = method.parameterAnnotations.get(key);
333 if (arg == null || !arg.element()) {
334 break;
335 }
336 if (!parameterNames.remove(key)) {
337 break;
338 }
339 }
340
341 // All parameters found! We have a match!
342 if (parameterNames.isEmpty()) {
343 return method;
344 }
345 */
346 return method;
347 }
348 return null;
349 }
350
351 private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) {
352 LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>();
353 NodeList childNodes = element.getChildNodes();
354 String lastTag = null;
355 for (int i = 0; i < childNodes.getLength(); i++) {
356 Node node = childNodes.item(i);
357 if (node.getNodeType() == Node.ELEMENT_NODE) {
358 Element el = (Element) node;
359 String tag = el.getLocalName();
360 ArrayList<Element> els = elements.get(tag);
361 if (els == null) {
362 els = new ArrayList<Element>();
363 elements.put(el.getLocalName(), els);
364 els.add(el);
365 lastTag = tag;
366 }
367 else {
368 // add to array if the elements are consecutive
369 if (tag.equals(lastTag)) {
370 els.add(el);
371 lastTag = tag;
372 }
373 }
374 }
375 }
376 return elements;
377 }
378
379 private HashMap<String, Object> getArugmentsFromAttributes(Element element) {
380 HashMap<String, Object> attributes = new HashMap<String, Object>();
381 NamedNodeMap childNodes = element.getAttributes();
382 for (int i = 0; i < childNodes.getLength(); i++) {
383 Node node = childNodes.item(i);
384 if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
385 Attr attr = (Attr) node;
386
387 String str = attr.getValue();
388 Object value = str;
389
390 // If the value starts with # then it's a bean reference
391 if (str.startsWith("#")) {
392 str = str.substring(1);
393 // Support using ## to escape the bean reference feature.
394 if (!str.startsWith("#")) {
395 value = new RuntimeBeanReference(str);
396 }
397 }
398
399 attributes.put(attr.getName(), value);
400 }
401 }
402 return attributes;
403 }
404
405 /**
406 * Finds all the methods on the clazz that match the name and which have the
407 * {@see Fluent} annotation and whoes parameters have the {@see FluentArg}
408 * annotation.
409 *
410 * @param clazz
411 * @param name
412 * @return
413 */
414 private ArrayList<MethodInfo> findFluentMethodsWithName(Class clazz, String name) {
415 ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>();
416 Method[] methods = clazz.getMethods();
417 for (int i = 0; i < methods.length; i++) {
418 Method method = methods[i];
419 if (!method.isAnnotationPresent(Fluent.class)) {
420 continue;
421 }
422
423 // Use the fluent supplied name for the action, or the method name if not set.
424 Fluent fluentAnnotation = method.getAnnotation(Fluent.class);
425 if (StringUtils.hasText(fluentAnnotation.value()) ?
426 name.equals(fluentAnnotation.value()) :
427 name.equals(method.getName())) {
428
429 LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>();
430 LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>();
431 Class<?>[] parameters = method.getParameterTypes();
432 for (int j = 0; j < parameters.length; j++) {
433 Class<?> parameter = parameters[j];
434 FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j);
435 if (annotation != null) {
436 map.put(annotation.value(), parameter);
437 amap.put(annotation.value(), annotation);
438 }
439 else {
440 break;
441 }
442 }
443
444 // If all the parameters were annotated...
445 if (parameters.length == map.size()) {
446 rc.add(new MethodInfo(method, fluentAnnotation, map, amap));
447 }
448 }
449 }
450 return rc;
451 }
452
453 private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) {
454 Annotation[] annotations = method.getParameterAnnotations()[index];
455 for (int i = 0; i < annotations.length; i++) {
456 if (annotationClass.isAssignableFrom(annotations[i].getClass())) {
457 return (T) annotations[i];
458 }
459 }
460 return null;
461 }
462
463 }