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