001 package org.apache.camel.spring.builder;
002
003 import java.lang.annotation.Annotation;
004 import java.lang.reflect.Array;
005 import java.lang.reflect.Method;
006 import java.util.ArrayList;
007 import java.util.Collections;
008 import java.util.Comparator;
009 import java.util.HashMap;
010 import java.util.HashSet;
011 import java.util.LinkedHashMap;
012 import java.util.List;
013 import java.util.Set;
014
015 import org.apache.camel.builder.Fluent;
016 import org.apache.camel.builder.FluentArg;
017 import org.apache.camel.builder.RouteBuilder;
018 import org.springframework.beans.SimpleTypeConverter;
019 import org.springframework.beans.factory.config.RuntimeBeanReference;
020 import org.springframework.beans.factory.support.AbstractBeanDefinition;
021 import org.springframework.beans.factory.support.BeanDefinitionBuilder;
022 import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
023 import org.springframework.beans.factory.xml.ParserContext;
024 import org.springframework.util.StringUtils;
025 import org.springframework.util.xml.DomUtils;
026 import org.w3c.dom.Attr;
027 import org.w3c.dom.Element;
028 import org.w3c.dom.NamedNodeMap;
029 import org.w3c.dom.Node;
030 import org.w3c.dom.NodeList;
031
032 public class CamelBeanDefinitionParser extends AbstractBeanDefinitionParser {
033
034 protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
035 BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(RouteBuilderFactory.class);
036
037 List childElements = DomUtils.getChildElementsByTagName(element, "route");
038 ArrayList<BuilderStatement> routes = new ArrayList<BuilderStatement>(childElements.size());
039
040 if (childElements != null && childElements.size() > 0) {
041 for (int i = 0; i < childElements.size(); ++i) {
042 Element routeElement = (Element) childElements.get(i);
043
044 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
045 Class type = parseBuilderElement(routeElement, RouteBuilder.class, actions);
046 BuilderStatement statement = new BuilderStatement();
047 statement.setReturnType(type);
048 statement.setActions(actions);
049 routes.add(statement);
050 }
051 }
052
053 factory.addPropertyValue("routes", routes);
054 return factory.getBeanDefinition();
055 }
056
057 /**
058 * Use reflection to figure out what is the valid next element.
059 * @param builder TODO
060 * @param routeElement
061 *
062 * @return
063 */
064 private Class parseBuilderElement(Element element, Class<RouteBuilder> builder, ArrayList<BuilderAction> actions) {
065 Class currentBuilder = builder;
066 NodeList childElements = element.getChildNodes();
067 Element previousElement = null;
068 for (int i = 0; i < childElements.getLength(); ++i) {
069 Node node = childElements.item(i);
070 if (node.getNodeType() == Node.ELEMENT_NODE) {
071 currentBuilder = parseAction(currentBuilder, actions, (Element) node, previousElement);
072 previousElement = (Element) node;
073 BuilderAction action = actions.get(actions.size()-1);
074
075 if( action.getMethodInfo().methodAnnotation.nestedActions() ) {
076 currentBuilder = parseBuilderElement((Element) node, currentBuilder, actions);
077 } else {
078 // Make sure the there are no child elements.
079 if( hasChildElements(node) ) {
080 throw new IllegalArgumentException("The element "+node.getLocalName()+" should not have any child elements.");
081 }
082 }
083
084 }
085 }
086
087 // Add the builder actions that are annotated with @Fluent(callOnElementEnd=true)
088 if( currentBuilder!=null ) {
089 Method[] methods = currentBuilder.getMethods();
090 for (int i = 0; i < methods.length; i++) {
091 Method method = methods[i];
092 Fluent annotation = method.getAnnotation(Fluent.class);
093 if( annotation!=null && annotation.callOnElementEnd() ) {
094
095 if( method.getParameterTypes().length > 0 ) {
096 throw new RuntimeException("Only methods with no parameters can annotated with @Fluent(callOnElementEnd=true): "+method);
097 }
098
099 MethodInfo methodInfo = new MethodInfo(method, annotation, new LinkedHashMap<String, Class>(), new LinkedHashMap<String, FluentArg>());
100 actions.add(new BuilderAction(methodInfo, new HashMap<String, Object>()));
101 currentBuilder = method.getReturnType();
102 }
103 }
104 }
105 return currentBuilder;
106 }
107
108 private boolean hasChildElements(Node node) {
109 NodeList nl = node.getChildNodes();
110 for (int j = 0; j < nl.getLength(); ++j) {
111 if( nl.item(j).getNodeType() == Node.ELEMENT_NODE ) {
112 return true;
113 }
114 }
115 return false;
116 }
117
118 private Class parseAction(Class currentBuilder, ArrayList<BuilderAction> actions, Element element, Element previousElement) {
119
120 String actionName = element.getLocalName();
121
122 // Get a list of method names that match the action.
123 ArrayList<MethodInfo> methods = findFluentMethodsWithName(currentBuilder, element.getLocalName());
124 if (methods.isEmpty()) {
125 throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
126 }
127
128 // Pick the best method out of the list. Sort by argument length. Pick
129 // first longest match.
130 Collections.sort(methods, new Comparator<MethodInfo>() {
131 public int compare(MethodInfo m1, MethodInfo m2) {
132 return m1.method.getParameterTypes().length - m2.method.getParameterTypes().length;
133 }
134 });
135
136 // Build the possible list of arguments from the attributes and child
137 // elements
138 HashMap<String, Object> attributeArguments = getArugmentsFromAttributes(element);
139 HashMap<String, ArrayList<Element>> elementArguments = getArgumentsFromElements(element);
140
141 // Find the first method that we can supply arguments for.
142 MethodInfo match = null;
143 match = findMethodMatch(methods, attributeArguments.keySet(), elementArguments.keySet());
144 if (match == null)
145 throw new IllegalActionException(actionName, previousElement == null ? null : previousElement.getLocalName());
146
147 // Move element arguments into the attributeArguments map if needed.
148 Set<String> parameterNames = new HashSet<String>(match.parameters.keySet());
149 parameterNames.removeAll(attributeArguments.keySet());
150 for (String key : parameterNames) {
151 ArrayList<Element> elements = elementArguments.get(key);
152 Class clazz = match.parameters.get(key);
153 Object value = convertTo(elements, clazz);
154 attributeArguments.put(key, value);
155 for (Element el : elements) {
156 // remove the argument nodes so that they don't get interpreted as
157 // actions.
158 el.getParentNode().removeChild(el);
159 }
160 }
161
162 actions.add(new BuilderAction(match, attributeArguments));
163 return match.method.getReturnType();
164 }
165
166 private Object convertTo(ArrayList<Element> elements, Class clazz) {
167
168 if( clazz.isArray() || elements.size() > 1 ) {
169 Object array = Array.newInstance(clazz.getComponentType(), elements.size());
170 for( int i=0; i < elements.size(); i ++ ) {
171 ArrayList<Element> e = new ArrayList<Element>(1);
172 e.add(elements.get(i));
173 Object value = convertTo(e, clazz.getComponentType());
174 Array.set(array, i, value);
175 }
176 return array;
177 } else {
178
179 Element element = elements.get(0);
180 String ref = element.getAttribute("ref");
181 if( StringUtils.hasText(ref) ) {
182 return new RuntimeBeanReference(ref);
183 }
184
185 // Use a builder to create the value..
186 if( hasChildElements(element) ) {
187
188 ArrayList<BuilderAction> actions = new ArrayList<BuilderAction>();
189 Class type = parseBuilderElement(element, RouteBuilder.class, actions);
190 BuilderStatement statement = new BuilderStatement();
191 statement.setReturnType(type);
192 statement.setActions(actions);
193
194 if( !clazz.isAssignableFrom( statement.getReturnType() ) ) {
195 throw new IllegalStateException("Builder does not produce object of expected type: "+clazz.getName());
196 }
197
198 return statement;
199 } else {
200 // Just use the text in the element as the value.
201 SimpleTypeConverter converter = new SimpleTypeConverter();
202 return converter.convertIfNecessary(element.getTextContent(), clazz);
203 }
204 }
205 }
206
207 private MethodInfo findMethodMatch(ArrayList<MethodInfo> methods, Set<String> attributeNames, Set<String> elementNames) {
208 for (MethodInfo method : methods) {
209
210 // make sure all the given attribute parameters can be assigned via
211 // attributes
212 boolean miss = false;
213 for (String key : attributeNames) {
214 FluentArg arg = method.parameterAnnotations.get(key);
215 if (arg == null || !arg.attribute()) {
216 miss = true;
217 break;
218 }
219 }
220 if (miss)
221 continue; // Keep looking...
222
223 Set<String> parameterNames = new HashSet<String>(method.parameters.keySet());
224 parameterNames.removeAll(attributeNames);
225
226 // Bingo we found a match.
227 if (parameterNames.isEmpty()) {
228 return method;
229 }
230
231 // We may still be able to match using elements as parameters.
232 for (String key : elementNames) {
233 if (parameterNames.isEmpty()) {
234 break;
235 }
236 // We only want to use the first child elements as arguments,
237 // once we don't match, we can stop looking.
238 FluentArg arg = method.parameterAnnotations.get(key);
239 if (arg == null || !arg.element()) {
240 break;
241 }
242 if (!parameterNames.remove(key)) {
243 break;
244 }
245 }
246
247 // All parameters found! We have a match!
248 if (parameterNames.isEmpty()) {
249 return method;
250 }
251
252 }
253 return null;
254 }
255
256 private LinkedHashMap<String, ArrayList<Element>> getArgumentsFromElements(Element element) {
257 LinkedHashMap<String, ArrayList<Element>> elements = new LinkedHashMap<String, ArrayList<Element>>();
258 NodeList childNodes = element.getChildNodes();
259 String lastTag = null;
260 for (int i = 0; i < childNodes.getLength(); i++) {
261 Node node = childNodes.item(i);
262 if (node.getNodeType() == Node.ELEMENT_NODE) {
263 Element el = (Element) node;
264 String tag = el.getLocalName();
265 ArrayList<Element> els = elements.get(tag);
266 if (els == null) {
267 els = new ArrayList<Element>();
268 elements.put(el.getLocalName(), els);
269 els.add(el);
270 lastTag = tag;
271 } else {
272 // add to array if the elements are consecutive
273 if (tag.equals(lastTag)) {
274 els.add(el);
275 lastTag = tag;
276 }
277 }
278 }
279 }
280 return elements;
281 }
282
283 private HashMap<String, Object> getArugmentsFromAttributes(Element element) {
284 HashMap<String, Object> attributes = new HashMap<String, Object>();
285 NamedNodeMap childNodes = element.getAttributes();
286 for (int i = 0; i < childNodes.getLength(); i++) {
287 Node node = childNodes.item(i);
288 if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
289 Attr attr = (Attr) node;
290
291 String str = attr.getValue();
292 Object value = str;
293
294 // If the value starts with # then it's a bean reference
295 if (str.startsWith("#")) {
296 str = str.substring(1);
297 // Support using ## to escape the bean reference feature.
298 if (!str.startsWith("#")) {
299 value = new RuntimeBeanReference(str);
300 }
301 }
302
303 attributes.put(attr.getName(), value);
304 }
305 }
306 return attributes;
307 }
308
309 /**
310 * Finds all the methods on the clazz that match the name and which have the
311 * {@see Fluent} annotation and whoes parameters have the {@see FluentArg}
312 * annotation.
313 *
314 * @param clazz
315 * @param name
316 * @return
317 */
318 private ArrayList<MethodInfo> findFluentMethodsWithName(Class clazz, String name) {
319 ArrayList<MethodInfo> rc = new ArrayList<MethodInfo>();
320 Method[] methods = clazz.getMethods();
321 for (int i = 0; i < methods.length; i++) {
322 Method method = methods[i];
323 if (!method.isAnnotationPresent(Fluent.class)) {
324 continue;
325 }
326
327 // Use the fluent supplied name for the action, or the method name if not set.
328 Fluent fluentAnnotation = method.getAnnotation(Fluent.class);
329 if ( StringUtils.hasText(fluentAnnotation.value()) ?
330 name.equals(fluentAnnotation.value()) :
331 name.equals(method.getName()) ) {
332
333 LinkedHashMap<String, Class> map = new LinkedHashMap<String, Class>();
334 LinkedHashMap<String, FluentArg> amap = new LinkedHashMap<String, FluentArg>();
335 Class<?>[] parameters = method.getParameterTypes();
336 for (int j = 0; j < parameters.length; j++) {
337 Class<?> parameter = parameters[j];
338 FluentArg annotation = getParameterAnnotation(FluentArg.class, method, j);
339 if (annotation != null) {
340 map.put(annotation.value(), parameter);
341 amap.put(annotation.value(), annotation);
342 } else {
343 break;
344 }
345 }
346
347 // If all the parameters were annotated...
348 if (parameters.length == map.size()) {
349 rc.add(new MethodInfo(method, fluentAnnotation, map, amap));
350 }
351 }
352 }
353 return rc;
354 }
355
356 private <T> T getParameterAnnotation(Class<T> annotationClass, Method method, int index) {
357 Annotation[] annotations = method.getParameterAnnotations()[index];
358 for (int i = 0; i < annotations.length; i++) {
359 if (annotationClass.isAssignableFrom(annotations[i].getClass())) {
360 return (T) annotations[i];
361 }
362 }
363 return null;
364 }
365
366 }