/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.util.component;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.camel.util.ObjectHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Parser base class for generating ApiMethod enumerations.
*/
public abstract class ApiMethodParser<T> {
// also used by JavadocApiMethodGeneratorMojo
public static final Pattern ARGS_PATTERN = Pattern.compile("\\s*([^<\\s]+)\\s*(<[^>]+>)?\\s+([^\\s,]+)\\s*,?");
private static final String METHOD_PREFIX = "^(\\s*(public|final|synchronized|native)\\s+)*(\\s*<[^>]>)?\\s*(\\S+)\\s+([^\\(]+\\s*)\\(";
private static final Pattern METHOD_PATTERN = Pattern.compile("\\s*([^<\\s]+)\\s*(<[^>]+>)?\\s+(\\S+)\\s*\\(\\s*([\\S\\s,]*)\\)\\s*;?\\s*");
private static final String JAVA_LANG = "java.lang.";
private static final Map<String, Class<?>> PRIMITIVE_TYPES;
static {
PRIMITIVE_TYPES = new HashMap<String, Class<?>>();
PRIMITIVE_TYPES.put("int", Integer.TYPE);
PRIMITIVE_TYPES.put("long", Long.TYPE);
PRIMITIVE_TYPES.put("double", Double.TYPE);
PRIMITIVE_TYPES.put("float", Float.TYPE);
PRIMITIVE_TYPES.put("boolean", Boolean.TYPE);
PRIMITIVE_TYPES.put("char", Character.TYPE);
PRIMITIVE_TYPES.put("byte", Byte.TYPE);
PRIMITIVE_TYPES.put("void", Void.TYPE);
PRIMITIVE_TYPES.put("short", Short.TYPE);
}
private final Logger log = LoggerFactory.getLogger(getClass());
private final Class<T> proxyType;
private List<String> signatures;
private ClassLoader classLoader = ApiMethodParser.class.getClassLoader();
public ApiMethodParser(Class<T> proxyType) {
this.proxyType = proxyType;
}
public Class<T> getProxyType() {
return proxyType;
}
public final List<String> getSignatures() {
return signatures;
}
public final void setSignatures(List<String> signatures) {
this.signatures = new ArrayList<String>();
this.signatures.addAll(signatures);
}
public final ClassLoader getClassLoader() {
return classLoader;
}
public final void setClassLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
}
/**
* Parses the method signatures from {@code getSignatures()}.
* @return list of Api methods as {@link ApiMethodModel}
*/
public final List<ApiMethodModel> parse() {
// parse sorted signatures and generate descriptions
List<ApiMethodModel> result = new ArrayList<ApiMethodModel>();
for (String signature : signatures) {
// skip comment or empty lines
if (signature.startsWith("##") || ObjectHelper.isEmpty(signature)) {
continue;
}
// remove all modifiers and type parameters for method
signature = signature.replaceAll(METHOD_PREFIX, "$4 $5(");
// remove all final modifiers for arguments
signature = signature.replaceAll("(\\(|,\\s*)final\\s+", "$1");
// remove all redundant spaces in generic parameters
signature = signature.replaceAll("\\s*<\\s*", "<").replaceAll("\\s*>", ">");
log.debug("Processing " + signature);
final Matcher methodMatcher = METHOD_PATTERN.matcher(signature);
if (!methodMatcher.matches()) {
throw new IllegalArgumentException("Invalid method signature " + signature);
}
// ignore generic type parameters in result, if any
final Class<?> resultType = forName(methodMatcher.group(1));
final String name = methodMatcher.group(3);
final String argSignature = methodMatcher.group(4);
final List<ApiMethodArg> arguments = new ArrayList<ApiMethodArg>();
final List<Class<?>> argTypes = new ArrayList<Class<?>>();
final Matcher argsMatcher = ARGS_PATTERN.matcher(argSignature);
while (argsMatcher.find()) {
final Class<?> type = forName(argsMatcher.group(1));
argTypes.add(type);
final String typeArgsGroup = argsMatcher.group(2);
final String typeArgs = typeArgsGroup != null
? typeArgsGroup.substring(1, typeArgsGroup.length() - 1).replaceAll(" ", "") : null;
arguments.add(new ApiMethodArg(argsMatcher.group(3), type, typeArgs));
}
Method method;
try {
method = proxyType.getMethod(name, argTypes.toArray(new Class<?>[argTypes.size()]));
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Method not found [" + signature + "] in type " + proxyType.getName());
}
result.add(new ApiMethodModel(name, resultType, arguments, method));
}
// allow derived classes to post process
result = processResults(result);
// check that argument names have the same type across methods
Map<String, Class<?>> allArguments = new HashMap<String, Class<?>>();
for (ApiMethodModel model : result) {
for (ApiMethodArg argument : model.getArguments()) {
String name = argument.getName();
Class<?> argClass = allArguments.get(name);
Class<?> type = argument.getType();
if (argClass == null) {
allArguments.put(name, type);
} else {
if (argClass != type) {
throw new IllegalArgumentException("Argument [" + name
+ "] is used in multiple methods with different types "
+ argClass.getCanonicalName() + ", " + type.getCanonicalName());
}
}
}
}
allArguments.clear();
result.sort(new Comparator<ApiMethodModel>() {
@Override
public int compare(ApiMethodModel model1, ApiMethodModel model2) {
final int nameCompare = model1.name.compareTo(model2.name);
if (nameCompare != 0) {
return nameCompare;
} else {
final int nArgs1 = model1.arguments.size();
final int nArgsCompare = nArgs1 - model2.arguments.size();
if (nArgsCompare != 0) {
return nArgsCompare;
} else {
// same number of args, compare arg names, kinda arbitrary to use alphabetized order
for (int i = 0; i < nArgs1; i++) {
final int argCompare = model1.arguments.get(i).getName().compareTo(model2.arguments.get(i).getName());
if (argCompare != 0) {
return argCompare;
}
}
// duplicate methods???
log.warn("Duplicate methods found [" + model1 + "], [" + model2 + "]");
return 0;
}
}
}
});
// assign unique names to every method model
final Map<String, Integer> dups = new HashMap<String, Integer>();
for (ApiMethodModel model : result) {
// locale independent upper case conversion
final String name = model.getName();
final char[] upperCase = new char[name.length()];
final char[] lowerCase = name.toCharArray();
for (int i = 0; i < upperCase.length; i++) {
upperCase[i] = Character.toUpperCase(lowerCase[i]);
}
String uniqueName = new String(upperCase);
Integer suffix = dups.get(uniqueName);
if (suffix == null) {
dups.put(uniqueName, 1);
} else {
dups.put(uniqueName, suffix + 1);
StringBuilder builder = new StringBuilder(uniqueName);
builder.append("_").append(suffix);
uniqueName = builder.toString();
}
model.uniqueName = uniqueName;
}
return result;
}
protected List<ApiMethodModel> processResults(List<ApiMethodModel> result) {
return result;
}
protected Class<?> forName(String className) {
try {
return forName(className, classLoader);
} catch (ClassNotFoundException e1) {
throw new IllegalArgumentException("Error loading class " + className);
}
}
public static Class<?> forName(String className, ClassLoader classLoader) throws ClassNotFoundException {
Class<?> result = null;
try {
// lookup primitive types first
result = PRIMITIVE_TYPES.get(className);
if (result == null) {
result = Class.forName(className, true, classLoader);
}
} catch (ClassNotFoundException e) {
// check if array type
if (className.endsWith("[]")) {
final int firstDim = className.indexOf('[');
final int nDimensions = (className.length() - firstDim) / 2;
result = Array.newInstance(forName(className.substring(0, firstDim), classLoader), new int[nDimensions]).getClass();
} else if (className.indexOf('.') != -1) {
// try replacing last '.' with $ to look for inner classes
String innerClass = className;
while (result == null && innerClass.indexOf('.') != -1) {
int endIndex = innerClass.lastIndexOf('.');
innerClass = innerClass.substring(0, endIndex) + "$" + innerClass.substring(endIndex + 1);
try {
result = Class.forName(innerClass, true, classLoader);
} catch (ClassNotFoundException ignore) {
// ignore
}
}
}
if (result == null && !className.startsWith(JAVA_LANG)) {
// try loading from default Java package java.lang
try {
result = forName(JAVA_LANG + className, classLoader);
} catch (ClassNotFoundException ignore) {
// ignore
}
}
}
if (result == null) {
throw new ClassNotFoundException(className);
}
return result;
}
public static final class ApiMethodModel {
private final String name;
private final Class<?> resultType;
private final List<ApiMethodArg> arguments;
private final Method method;
private String uniqueName;
protected ApiMethodModel(String name, Class<?> resultType, List<ApiMethodArg> arguments, Method method) {
this.name = name;
this.resultType = resultType;
this.arguments = arguments;
this.method = method;
}
protected ApiMethodModel(String uniqueName, String name, Class<?> resultType, List<ApiMethodArg> arguments, Method method) {
this.name = name;
this.uniqueName = uniqueName;
this.resultType = resultType;
this.arguments = arguments;
this.method = method;
}
public String getUniqueName() {
return uniqueName;
}
public String getName() {
return name;
}
public Class<?> getResultType() {
return resultType;
}
public Method getMethod() {
return method;
}
public List<ApiMethodArg> getArguments() {
return arguments;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(resultType.getName()).append(" ");
builder.append(name).append("(");
for (ApiMethodArg argument : arguments) {
builder.append(argument.getType().getCanonicalName()).append(" ");
builder.append(argument.getName()).append(", ");
}
if (!arguments.isEmpty()) {
builder.delete(builder.length() - 2, builder.length());
}
builder.append(");");
return builder.toString();
}
}
}