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.Comparator;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.regex.Matcher;
027import java.util.regex.Pattern;
028
029import org.apache.camel.util.ObjectHelper;
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            // skip comment or empty lines
102            if (signature.startsWith("##") || ObjectHelper.isEmpty(signature)) {
103                continue;
104            }
105
106            // remove all modifiers and type parameters for method
107            signature = signature.replaceAll(METHOD_PREFIX, "$4 $5(");
108            // remove all final modifiers for arguments
109            signature = signature.replaceAll("(\\(|,\\s*)final\\s+", "$1");
110            // remove all redundant spaces in generic parameters
111            signature = signature.replaceAll("\\s*<\\s*", "<").replaceAll("\\s*>", ">");
112
113            log.debug("Processing " + signature);
114
115            final Matcher methodMatcher = METHOD_PATTERN.matcher(signature);
116            if (!methodMatcher.matches()) {
117                throw new IllegalArgumentException("Invalid method signature " + signature);
118            }
119
120            // ignore generic type parameters in result, if any
121            final Class<?> resultType = forName(methodMatcher.group(1));
122            final String name = methodMatcher.group(3);
123            final String argSignature = methodMatcher.group(4);
124
125            final List<ApiMethodArg> arguments = new ArrayList<ApiMethodArg>();
126            final List<Class<?>> argTypes = new ArrayList<Class<?>>();
127
128            final Matcher argsMatcher = ARGS_PATTERN.matcher(argSignature);
129            while (argsMatcher.find()) {
130
131                final Class<?> type = forName(argsMatcher.group(1));
132                argTypes.add(type);
133
134                final String typeArgsGroup = argsMatcher.group(2);
135                final String typeArgs = typeArgsGroup != null
136                    ? typeArgsGroup.substring(1, typeArgsGroup.length() - 1).replaceAll(" ", "") : null;
137                arguments.add(new ApiMethodArg(argsMatcher.group(3), type, typeArgs));
138            }
139
140            Method method;
141            try {
142                method = proxyType.getMethod(name, argTypes.toArray(new Class<?>[argTypes.size()]));
143            } catch (NoSuchMethodException e) {
144                throw new IllegalArgumentException("Method not found [" + signature + "] in type " + proxyType.getName());
145            }
146            result.add(new ApiMethodModel(name, resultType, arguments, method));
147        }
148
149        // allow derived classes to post process
150        result = processResults(result);
151
152        // check that argument names have the same type across methods
153        Map<String, Class<?>> allArguments = new HashMap<String, Class<?>>();
154        for (ApiMethodModel model : result) {
155            for (ApiMethodArg argument : model.getArguments()) {
156                String name = argument.getName();
157                Class<?> argClass = allArguments.get(name);
158                Class<?> type = argument.getType();
159                if (argClass == null) {
160                    allArguments.put(name, type);
161                } else {
162                    if (argClass != type) {
163                        throw new IllegalArgumentException("Argument [" + name 
164                                + "] is used in multiple methods with different types " 
165                                + argClass.getCanonicalName() + ", " + type.getCanonicalName());
166                    }
167                }
168            }
169        }
170        allArguments.clear();
171
172        result.sort(new Comparator<ApiMethodModel>() {
173            @Override
174            public int compare(ApiMethodModel model1, ApiMethodModel model2) {
175                final int nameCompare = model1.name.compareTo(model2.name);
176                if (nameCompare != 0) {
177                    return nameCompare;
178                } else {
179
180                    final int nArgs1 = model1.arguments.size();
181                    final int nArgsCompare = nArgs1 - model2.arguments.size();
182                    if (nArgsCompare != 0) {
183                        return nArgsCompare;
184                    } else {
185                        // same number of args, compare arg names, kinda arbitrary to use alphabetized order
186                        for (int i = 0; i < nArgs1; i++) {
187                            final int argCompare = model1.arguments.get(i).getName().compareTo(model2.arguments.get(i).getName());
188                            if (argCompare != 0) {
189                                return argCompare;
190                            }
191                        }
192                        // duplicate methods???
193                        log.warn("Duplicate methods found [" + model1 + "], [" + model2 + "]");
194                        return 0;
195                    }
196                }
197            }
198        });
199
200        // assign unique names to every method model
201        final Map<String, Integer> dups = new HashMap<String, Integer>();
202        for (ApiMethodModel model : result) {
203            // locale independent upper case conversion
204            final String name = model.getName();
205            final char[] upperCase = new char[name.length()];
206            final char[] lowerCase = name.toCharArray();
207            for (int i = 0; i < upperCase.length; i++) {
208                upperCase[i] = Character.toUpperCase(lowerCase[i]);
209            }
210            String uniqueName = new String(upperCase);
211
212            Integer suffix = dups.get(uniqueName);
213            if (suffix == null) {
214                dups.put(uniqueName, 1);
215            } else {
216                dups.put(uniqueName, suffix + 1);
217                StringBuilder builder = new StringBuilder(uniqueName);
218                builder.append("_").append(suffix);
219                uniqueName = builder.toString();
220            }
221            model.uniqueName = uniqueName;
222        }
223        return result;
224    }
225
226    protected List<ApiMethodModel> processResults(List<ApiMethodModel> result) {
227        return result;
228    }
229
230    protected Class<?> forName(String className) {
231        try {
232            return forName(className, classLoader);
233        } catch (ClassNotFoundException e1) {
234            throw new IllegalArgumentException("Error loading class " + className);
235        }
236    }
237
238    public static Class<?> forName(String className, ClassLoader classLoader) throws ClassNotFoundException {
239        Class<?> result = null;
240        try {
241            // lookup primitive types first
242            result = PRIMITIVE_TYPES.get(className);
243            if (result == null) {
244                result = Class.forName(className, true, classLoader);
245            }
246        } catch (ClassNotFoundException e) {
247            // check if array type
248            if (className.endsWith("[]")) {
249                final int firstDim = className.indexOf('[');
250                final int nDimensions = (className.length() - firstDim) / 2;
251                result = Array.newInstance(forName(className.substring(0, firstDim), classLoader), new int[nDimensions]).getClass();
252            } else if (className.indexOf('.') != -1) {
253                // try replacing last '.' with $ to look for inner classes
254                String innerClass = className;
255                while (result == null && innerClass.indexOf('.') != -1) {
256                    int endIndex = innerClass.lastIndexOf('.');
257                    innerClass = innerClass.substring(0, endIndex) + "$" + innerClass.substring(endIndex + 1);
258                    try {
259                        result = Class.forName(innerClass, true, classLoader);
260                    } catch (ClassNotFoundException ignore) {
261                        // ignore
262                    }
263                }
264            }
265            if (result == null && !className.startsWith(JAVA_LANG)) {
266                // try loading from default Java package java.lang
267                try {
268                    result = forName(JAVA_LANG + className, classLoader);
269                } catch (ClassNotFoundException ignore) {
270                    // ignore
271                }
272            }
273        }
274
275        if (result == null) {
276            throw new ClassNotFoundException(className);
277        }
278
279        return result;
280    }
281
282    public static final class ApiMethodModel {
283        private final String name;
284        private final Class<?> resultType;
285        private final List<ApiMethodArg> arguments;
286        private final Method method;
287
288        private String uniqueName;
289
290        protected ApiMethodModel(String name, Class<?> resultType, List<ApiMethodArg> arguments, Method method) {
291            this.name = name;
292            this.resultType = resultType;
293            this.arguments = arguments;
294            this.method = method;
295        }
296
297        protected ApiMethodModel(String uniqueName, String name, Class<?> resultType, List<ApiMethodArg> arguments, Method method) {
298            this.name = name;
299            this.uniqueName = uniqueName;
300            this.resultType = resultType;
301            this.arguments = arguments;
302            this.method = method;
303        }
304
305        public String getUniqueName() {
306            return uniqueName;
307        }
308
309        public String getName() {
310            return name;
311        }
312
313        public Class<?> getResultType() {
314            return resultType;
315        }
316
317        public Method getMethod() {
318            return method;
319        }
320
321        public List<ApiMethodArg> getArguments() {
322            return arguments;
323        }
324
325        @Override
326        public String toString() {
327            StringBuilder builder = new StringBuilder();
328            builder.append(resultType.getName()).append(" ");
329            builder.append(name).append("(");
330            for (ApiMethodArg argument : arguments) {
331                builder.append(argument.getType().getCanonicalName()).append(" ");
332                builder.append(argument.getName()).append(", ");
333            }
334            if (!arguments.isEmpty()) {
335                builder.delete(builder.length() - 2, builder.length());
336            }
337            builder.append(");");
338            return builder.toString();
339        }
340    }
341}