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