001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.util.component;
018
019import java.lang.reflect.Array;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.Comparator;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027import java.util.regex.Matcher;
028import java.util.regex.Pattern;
029
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033/**
034 * Parser base class for generating ApiMethod enumerations.
035 */
036public abstract class ApiMethodParser<T> {
037
038    // also used by JavadocApiMethodGeneratorMojo
039    public static final Pattern ARGS_PATTERN = Pattern.compile("\\s*([^<\\s]+)\\s*(<[^>]+>)?\\s+([^\\s,]+)\\s*,?");
040
041    private static final String METHOD_PREFIX = "^(\\s*(public|final|synchronized|native)\\s+)*(\\s*<[^>]>)?\\s*(\\S+)\\s+([^\\(]+\\s*)\\(";
042    private static final Pattern METHOD_PATTERN = Pattern.compile("\\s*([^<\\s]+)\\s*(<[^>]+>)?\\s+(\\S+)\\s*\\(\\s*([\\S\\s,]*)\\)\\s*;?\\s*");
043
044    private static final String JAVA_LANG = "java.lang.";
045    private static final Map<String, Class<?>> PRIMITIVE_TYPES;
046
047    static {
048        PRIMITIVE_TYPES = new HashMap<String, Class<?>>();
049        PRIMITIVE_TYPES.put("int", Integer.TYPE);
050        PRIMITIVE_TYPES.put("long", Long.TYPE);
051        PRIMITIVE_TYPES.put("double", Double.TYPE);
052        PRIMITIVE_TYPES.put("float", Float.TYPE);
053        PRIMITIVE_TYPES.put("boolean", Boolean.TYPE);
054        PRIMITIVE_TYPES.put("char", Character.TYPE);
055        PRIMITIVE_TYPES.put("byte", Byte.TYPE);
056        PRIMITIVE_TYPES.put("void", Void.TYPE);
057        PRIMITIVE_TYPES.put("short", Short.TYPE);
058    }
059
060
061    private final Logger log = LoggerFactory.getLogger(getClass());
062
063    private final Class<T> proxyType;
064    private List<String> signatures;
065    private ClassLoader classLoader = ApiMethodParser.class.getClassLoader();
066
067    public ApiMethodParser(Class<T> proxyType) {
068        this.proxyType = proxyType;
069    }
070
071    public Class<T> getProxyType() {
072        return proxyType;
073    }
074
075    public final List<String> getSignatures() {
076        return signatures;
077    }
078
079    public final void setSignatures(List<String> signatures) {
080        this.signatures = new ArrayList<String>();
081        this.signatures.addAll(signatures);
082    }
083
084    public final ClassLoader getClassLoader() {
085        return classLoader;
086    }
087
088    public final void setClassLoader(ClassLoader classLoader) {
089        this.classLoader = classLoader;
090    }
091
092    /**
093     * Parses the method signatures from {@code getSignatures()}.
094     * @return list of Api methods as {@link ApiMethodModel}
095     */
096    public final List<ApiMethodModel> parse() {
097        // parse sorted signatures and generate descriptions
098        List<ApiMethodModel> result = new ArrayList<ApiMethodModel>();
099        for (String signature : signatures) {
100
101            // remove all modifiers and type parameters for method
102            signature = signature.replaceAll(METHOD_PREFIX, "$4 $5(");
103            // remove all final modifiers for arguments
104            signature = signature.replaceAll("(\\(|,\\s*)final\\s+", "$1");
105            // remove all redundant spaces in generic parameters
106            signature = signature.replaceAll("\\s*<\\s*", "<").replaceAll("\\s*>", ">");
107
108            log.debug("Processing " + signature);
109
110            final Matcher methodMatcher = METHOD_PATTERN.matcher(signature);
111            if (!methodMatcher.matches()) {
112                throw new IllegalArgumentException("Invalid method signature " + signature);
113            }
114
115            // ignore generic type parameters in result, if any
116            final Class<?> resultType = forName(methodMatcher.group(1));
117            final String name = methodMatcher.group(3);
118            final String argSignature = methodMatcher.group(4);
119
120            final List<Argument> arguments = new ArrayList<Argument>();
121            final List<Class<?>> argTypes = new ArrayList<Class<?>>();
122
123            final Matcher argsMatcher = ARGS_PATTERN.matcher(argSignature);
124            while (argsMatcher.find()) {
125
126                final Class<?> type = forName(argsMatcher.group(1));
127                argTypes.add(type);
128
129                final String typeArgsGroup = argsMatcher.group(2);
130                final String typeArgs = typeArgsGroup != null
131                    ? typeArgsGroup.substring(1, typeArgsGroup.length() - 1).replaceAll(" ", "") : null;
132                arguments.add(new Argument(argsMatcher.group(3), type, typeArgs));
133            }
134
135            Method method;
136            try {
137                method = proxyType.getMethod(name, argTypes.toArray(new Class<?>[argTypes.size()]));
138            } catch (NoSuchMethodException e) {
139                throw new IllegalArgumentException("Method not found [" + signature + "] in type " + proxyType.getName());
140            }
141            result.add(new ApiMethodModel(name, resultType, arguments, method));
142        }
143
144        // allow derived classes to post process
145        result = processResults(result);
146
147        // check that argument names have the same type across methods
148        Map<String, Class<?>> allArguments = new HashMap<String, Class<?>>();
149        for (ApiMethodModel model : result) {
150            for (Argument argument : model.getArguments()) {
151                String name = argument.getName();
152                Class<?> argClass = allArguments.get(name);
153                Class<?> type = argument.getType();
154                if (argClass == null) {
155                    allArguments.put(name, type);
156                } else {
157                    if (argClass != type) {
158                        throw new IllegalArgumentException("Argument [" + name 
159                                + "] is used in multiple methods with different types " 
160                                + argClass.getCanonicalName() + ", " + type.getCanonicalName());
161                    }
162                }
163            }
164        }
165        allArguments.clear();
166
167        Collections.sort(result, new Comparator<ApiMethodModel>() {
168            @Override
169            public int compare(ApiMethodModel model1, ApiMethodModel model2) {
170                final int nameCompare = model1.name.compareTo(model2.name);
171                if (nameCompare != 0) {
172                    return nameCompare;
173                } else {
174
175                    final int nArgs1 = model1.arguments.size();
176                    final int nArgsCompare = nArgs1 - model2.arguments.size();
177                    if (nArgsCompare != 0) {
178                        return nArgsCompare;
179                    } else {
180                        // same number of args, compare arg names, kinda arbitrary to use alphabetized order
181                        for (int i = 0; i < nArgs1; i++) {
182                            final int argCompare = model1.arguments.get(i).name.compareTo(model2.arguments.get(i).name);
183                            if (argCompare != 0) {
184                                return argCompare;
185                            }
186                        }
187                        // duplicate methods???
188                        log.warn("Duplicate methods found [" + model1 + "], [" + model2 + "]");
189                        return 0;
190                    }
191                }
192            }
193        });
194
195        // assign unique names to every method model
196        final Map<String, Integer> dups = new HashMap<String, Integer>();
197        for (ApiMethodModel model : result) {
198            // locale independent upper case conversion
199            final String name = model.getName();
200            final char[] upperCase = new char[name.length()];
201            final char[] lowerCase = name.toCharArray();
202            for (int i = 0; i < upperCase.length; i++) {
203                upperCase[i] = Character.toUpperCase(lowerCase[i]);
204            }
205            String uniqueName = new String(upperCase);
206
207            Integer suffix = dups.get(uniqueName);
208            if (suffix == null) {
209                dups.put(uniqueName, 1);
210            } else {
211                dups.put(uniqueName, suffix + 1);
212                StringBuilder builder = new StringBuilder(uniqueName);
213                builder.append("_").append(suffix);
214                uniqueName = builder.toString();
215            }
216            model.uniqueName = uniqueName;
217        }
218        return result;
219    }
220
221    protected List<ApiMethodModel> processResults(List<ApiMethodModel> result) {
222        return result;
223    }
224
225    protected Class<?> forName(String className) {
226        try {
227            return forName(className, classLoader);
228        } catch (ClassNotFoundException e1) {
229            throw new IllegalArgumentException("Error loading class " + className);
230        }
231    }
232
233    public static Class<?> forName(String className, ClassLoader classLoader) throws ClassNotFoundException {
234        Class<?> result;
235        try {
236            // lookup primitive types first
237            result = PRIMITIVE_TYPES.get(className);
238            if (result == null) {
239                result = Class.forName(className, true, classLoader);
240            }
241        } catch (ClassNotFoundException e) {
242            // check if array type
243            if (className.endsWith("[]")) {
244                final int firstDim = className.indexOf('[');
245                final int nDimensions = (className.length() - firstDim) / 2;
246                return Array.newInstance(forName(className.substring(0, firstDim), classLoader), new int[nDimensions]).getClass();
247            }
248            // try loading from default Java package java.lang
249            result = Class.forName(JAVA_LANG + className, true, classLoader);
250        }
251
252        return result;
253    }
254
255    public static final class ApiMethodModel {
256        private final String name;
257        private final Class<?> resultType;
258        private final List<Argument> arguments;
259        private final Method method;
260
261        private String uniqueName;
262
263        protected ApiMethodModel(String name, Class<?> resultType, List<Argument> arguments, Method method) {
264            this.name = name;
265            this.resultType = resultType;
266            this.arguments = arguments;
267            this.method = method;
268        }
269
270        protected ApiMethodModel(String uniqueName, String name, Class<?> resultType, List<Argument> arguments, Method method) {
271            this.name = name;
272            this.uniqueName = uniqueName;
273            this.resultType = resultType;
274            this.arguments = arguments;
275            this.method = method;
276        }
277
278        public String getUniqueName() {
279            return uniqueName;
280        }
281
282        public String getName() {
283            return name;
284        }
285
286        public Class<?> getResultType() {
287            return resultType;
288        }
289
290        public Method getMethod() {
291            return method;
292        }
293
294        public List<Argument> getArguments() {
295            return arguments;
296        }
297
298        @Override
299        public String toString() {
300            StringBuilder builder = new StringBuilder();
301            builder.append(resultType.getName()).append(" ");
302            builder.append(name).append("(");
303            for (Argument argument : arguments) {
304                builder.append(argument.getType().getCanonicalName()).append(" ");
305                builder.append(argument.getName()).append(", ");
306            }
307            if (!arguments.isEmpty()) {
308                builder.delete(builder.length() - 2, builder.length());
309            }
310            builder.append(");");
311            return builder.toString();
312        }
313    }
314
315    public static final class Argument {
316        private final String name;
317        private final Class<?> type;
318        private final String typeArgs;
319
320        public Argument(String name, Class<?> type, String typeArgs) {
321            this.name = name;
322            this.type = type;
323            this.typeArgs = typeArgs;
324        }
325
326        public String getName() {
327            return name;
328        }
329
330        public Class<?> getType() {
331            return type;
332        }
333
334        public String getTypeArgs() {
335            return typeArgs;
336        }
337
338        @Override
339        public String toString() {
340            StringBuilder builder = new StringBuilder();
341            builder.append(type.getCanonicalName());
342            if (typeArgs != null) {
343                builder.append("<").append(typeArgs).append(">");
344            }
345            builder.append(" ").append(name);
346            return builder.toString();
347        }
348    }
349}