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