com.redhat.ceylon.common.tool.ToolLoader Maven / Gradle / Ivy
package com.redhat.ceylon.common.tool;
import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.TreeSet;
import com.redhat.ceylon.common.OSUtil;
import com.redhat.ceylon.common.tool.OptionModel.ArgumentType;
/**
* Responsible for locating a Class for a given tool name and constucting a
* {@link ToolModel} by reflection on that class.
* @author tom
*/
public abstract class ToolLoader {
public static final String SCRIPT_PREFIX = "SCRIPT:";
public static final String PLUGIN_PREFIX = "PLUGIN:";
protected final ClassLoader loader;
private Map> toolModels = new HashMap>();
public ToolLoader() {
this(ToolLoader.class.getClassLoader());
}
public ToolLoader(ClassLoader loader) {
this.loader = loader == null ? ClassLoader.getSystemClassLoader() : loader;
}
protected Class loadToolClass(final String toolName) {
String className = getToolClassName(toolName);
if (className == null) {
return null;
}
try {
Class toolClass = (Class)loader.loadClass(className);
return toolClass;
} catch (ClassNotFoundException e) {
return null;
}
}
protected String getToolClassName(final String toolName) {
List classNames = new ArrayList<>();
String className = null;
for (String cls : toolClassNames()) {
if (toolName.equals(getToolName(cls))) {
classNames.add(cls);
}
}
if (classNames.isEmpty()) {
return null;
}
className = classNames.get(0);
return className;
}
public ClassLoader loadModule(String name, String version) {
try {
// Ok, now for something really crappy to force loading of the required module
String loaderClassName;
if (loader.getClass().getName().equals("org.jboss.modules.ModuleClassLoader")
|| loader.getClass().getName().equals("ceylon.modules.jboss.runtime.CeylonModuleClassLoader")) {
// If we run using the Ceylon runtime
loaderClassName = "com.redhat.ceylon.compiler.java.runtime.tools.impl.JBossModuleLoader";
} else {
// If we run in a normal Java environment
loaderClassName = "com.redhat.ceylon.compiler.java.runtime.tools.impl.FlatpathModuleLoader";
}
Class> loaderClass = loader.loadClass(loaderClassName);
Constructor> loaderConstr = loaderClass.getConstructor(ClassLoader.class);
Object modLoader = loaderConstr.newInstance(loader);
Method loadMth = loaderClass.getMethod("loadModule", String.class, String.class);
ClassLoader mcl = (ClassLoader) loadMth.invoke(modLoader, name, version);
return mcl;
} catch (ReflectiveOperationException e) {
throw new ToolError("Could not load module '" + name + "/" + version + "' because: " + e.getCause().getMessage(), e) {};
}
}
/**
* Returns a ToolModel given the name of the tool, or null if no such tool is
* know to this tool loader.
*/
public synchronized ToolModel loadToolModel(String toolName) {
@SuppressWarnings("unchecked")
ToolModel loadedModel = (ToolModel) toolModels.get(toolName);
if(loadedModel == null){
loadedModel = loadToolModelMemoised(toolName);
toolModels.put(toolName, loadedModel);
}
return loadedModel;
}
private ToolModel loadToolModelMemoised(String toolName) {
String className = getToolClassName(toolName);
if(className != null && className.startsWith(SCRIPT_PREFIX)){
return loadScriptTool(className, toolName);
}else if(className != null && className.startsWith(PLUGIN_PREFIX)){
return loadPluginTool(className, toolName);
}else{
Class toolClass = loadToolClass(toolName);
if (toolClass != null) {
final ToolModel toolModel;
try {
toolModel = loadModel(toolClass, toolName);
} catch (ModelException e) {
throw e;
} catch (RuntimeException e) {
throw new ModelException("Failed to load model for tool " + toolName +"(" + toolClass + ")", e);
}
toolModel.setToolLoader(this);
return toolModel;
}
}
return null;
}
private ToolModel loadScriptTool(String className, String toolName) {
ScriptToolModel model = new ScriptToolModel(toolName, className.substring(7));
model.setToolLoader(this);
return model;
}
private ToolModel loadPluginTool(String className, String toolName) {
PluginToolModel model = new PluginToolModel(toolName, className.substring(7));
model.setToolLoader(this);
return model;
}
private ToolModel loadModel(Class cls, String toolName) {
checkClass(cls);
AnnotatedToolModel model = new AnnotatedToolModel(toolName);
model.setToolLoader(this);
model.setToolClass(cls);
return model;
}
protected void setupModel(AnnotatedToolModel model) {
Class cls = model.getToolClass();
// We use this Map because Java doesn't define the order that the
// declared methods will be returned in, but the order matters
TreeMap> orderedArgumentModels = new TreeMap>();
for (Method method : cls.getMethods()) {
addMethod(cls, model, method, orderedArgumentModels);
}
Entry> last = orderedArgumentModels.lastEntry();
if (last != null && last.getValue() instanceof SubtoolModel) {
model.setSubtoolModel((SubtoolModel>)last.getValue());
orderedArgumentModels.remove(last.getKey());
}
for (ArgumentModel> argumentModel : orderedArgumentModels.values()) {
if (argumentModel instanceof SubtoolModel) {
throw new ModelException("A @Subtool's order() must be greater than all @Argument order()s");
}
model.addArgument(argumentModel);
}
}
private ArgumentParser> getArgumentParser(Method setter, Class> setterType, boolean isSimpleType) {
Subtool subtool = setter.getAnnotation(Subtool.class);
ParsedBy pf = setter.getAnnotation(ParsedBy.class);
if (subtool != null) {
if (subtool.classes().length > 0) {
if (pf != null) {
throw new ModelException(setter + " annotated with both @Subtool(classes=...) and @ParsedBy");
}
return new ToolArgumentParser(MapToolLoader.fromClassNames(subtool.classes()));
}
}
if (pf != null) {
try {
return pf.value().newInstance();
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new ModelException("Error instantiating the given @ParserFactory", e);
}
}
return StandardArgumentParsers.forClass(setterType, this, isSimpleType);
}
private void addMethod(Class cls, ToolModel model,
Method method, Map> orderedArgumentModels) {
final Rest rest = method.getAnnotation(Rest.class);
if (rest != null) {
if (!isSetter(method)) {
throw new ModelException("Method " + method + " is annotated @Rest but is not a setter");
}
if (model.getRest() != null) {
throw new ModelException("Only one method may be annotated @Rest: " + model.getRest() + " and " + method);
}
model.setRest(method);
}
OptionModel optionModel = buildOption(model, method);
OptionModel optionArgumentModel = buildOptionArgument(model, method);
ArgumentModel argumentModel = buildArgument(method, orderedArgumentModels);
SubtoolModel subtoolModel = buildSubtool(method, orderedArgumentModels);
if (optionModel!= null) {
if (argumentModel != null || subtoolModel != null) {
throw new ModelException(method + " is annotated with both @Option and @Argument/@Subtool");
}
if (optionArgumentModel != null) {
throw new ModelException(method + " is annotated with both @Option and @OptionArgument");
}
checkDuplicateOption(cls, model, optionModel);
model.addOption(optionModel);
} else if (optionArgumentModel != null) {
if (argumentModel != null || subtoolModel != null) {
throw new ModelException(method + " is annotated with both @OptionArgument and @Argument/@Subtool");
}
checkDuplicateOption(cls, model, optionArgumentModel);
model.addOption(optionArgumentModel);
} else if (argumentModel != null) {
if (subtoolModel != null) {
throw new ModelException(method + " is annotated with both @Argument and @Subtool");
}
// We don't add it to the model here, it's in the orderedArgumentModels
}
}
private void checkDuplicateOption(Class cls,
ToolModel model, OptionModel> optionModel) {
if (model.getOption(optionModel.getLongName()) != null) {
throw new ModelException(cls + " has more than one binding for option " + optionModel.getLongName());
}
if (optionModel.getShortName() != null
&& model.getOptionByShort(optionModel.getShortName()) != null) {
throw new ModelException(cls + " has more than one binding for short option " + optionModel.getShortName());
}
}
private ArgumentModel buildPureOption(ToolModel> toolModel, Method method) {
ArgumentModel argumentModel;
argumentModel = new ArgumentModel();
argumentModel.setParser((ArgumentParser)getArgumentParser(method, boolean.class, true));
argumentModel.setToolModel(toolModel);
argumentModel.setSetter(method);
argumentModel.setType(boolean.class);
argumentModel.setMultiplicity(Multiplicity._0_OR_1);
return argumentModel;
}
protected void checkClass(Class extends Tool> cls) throws ModelException {
final int classModifiers = cls.getModifiers();
if (Modifier.isAbstract(classModifiers)) {
throw new ModelException("Tool " + cls + " is not concrete");
}
if (Modifier.isInterface(classModifiers)) {
throw new ModelException("Tool " + cls + " is not a class");
}
if (!Modifier.isPublic(classModifiers)) {
throw new ModelException("Tool " + cls + " is not public");
}
if (cls.getEnclosingClass() != null
&& !Modifier.isStatic(cls.getModifiers())) {
try {
cls.getConstructor(cls.getEnclosingClass());
} catch (NoSuchMethodException e) {
throw new ModelException("Tool " + cls + " does not have a public 0 argument constructor");
}
} else {
try {
cls.getConstructor();
} catch (NoSuchMethodException e) {
throw new ModelException("Tool " + cls + " does not have a public 0 argument constructor");
}
}
}
private boolean hasDescription(Method setter) {
return setter.getAnnotation(Description.class) != null;
}
private boolean isSetter(Method method) {
return method.getName().matches("set[A-Z0-9].*")
&& Modifier.isPublic(method.getModifiers())
&& method.getReturnType().equals(void.class)
&& method.getParameterTypes().length == 1;
}
protected String classNameToToolName(String className){
if(className.startsWith(SCRIPT_PREFIX)){
String name = className.substring(7);
int lastSep = className.lastIndexOf(File.separatorChar);
if(lastSep != -1)
name = name.substring(lastSep+1);
if(OSUtil.isWindows()) // strip the .bat
name = name.substring(0, name.length()-4);
return name;
} else if(className.startsWith(PLUGIN_PREFIX)){
String name = className.substring(7);
int lastSep = className.lastIndexOf(File.separatorChar);
if(lastSep != -1)
name = name.substring(lastSep+1);
name = name.substring(0, name.length()-7);
return name;
}
return camelCaseToDashes(className.replaceAll("^(.*\\.)?Ceylon(.*)Tool$", "$2"));
}
protected String camelCaseToDashes(String name) {
StringBuilder sb = new StringBuilder();
for (char ch : name.toCharArray()) {
if (Character.isUpperCase(ch)) {
if (sb.length() != 0) {
sb.append('-');
}
sb.append(Character.toLowerCase(ch));
} else {
sb.append(ch);
}
}
final String toolName = sb.toString();
return toolName;
}
private String getOptionName(String name, Method setter) {
if (name == null || name.isEmpty()) {
name = camelCaseToDashes(setter.getName().substring("set".length()));
}
return name;
}
private OptionModel buildOption(ToolModel> toolModel, final Method setter) {
Option option = setter.getAnnotation(Option.class);
if (option == null || setter.getAnnotation(OptionArgument.class) != null) {
return null;
}
if (!isSetter(setter)) {
throw new ModelException("Method " + setter + " is annotated with @Option but is not a setter");
}
if (!setter.getParameterTypes()[0].equals(Boolean.TYPE)) {
throw new ModelException("Method " + setter + " is annotated with @Option but has a non-boolean parameter");
}
OptionModel optionModel = new OptionModel();
optionModel.setLongName(getOptionName(option.longName(), setter));
char shortName = option.shortName();
if (shortName != Option.NO_SHORT) {
optionModel.setShortName(shortName);
}
optionModel.setArgumentType(OptionModel.ArgumentType.NOT_ALLOWED);
optionModel.setArgument(buildPureOption(toolModel, setter));
optionModel.getArgument().setOption(optionModel);
return optionModel;
}
private OptionModel buildOptionArgument(ToolModel> toolModel, final Method setter) {
OptionArgument optionArgument = setter.getAnnotation(OptionArgument.class);
if (optionArgument == null) {
return null;
}
if (!isSetter(setter)) {
throw new ModelException("Method " + setter + " is annotated with @OptionArgument but is not a setter");
}
Option option = setter.getAnnotation(Option.class);
boolean argumentOptional = option != null;
if (argumentOptional) {
if (setter.getParameterTypes()[0].isPrimitive()) {
throw new ModelException("Method " + setter + " is annotated with @OptionArgument and @Option has primitive parameter type");
}
if (optionArgument.shortName() != option.shortName() && optionArgument.shortName() != OptionArgument.NO_SHORT) {
throw new ModelException("Method " + setter + " is annotated with @OptionArgument and @Option, but their shortName()s differ");
}
if (!optionArgument.longName().equals(option.longName())) {
throw new ModelException("Method " + setter + " is annotated with @OptionArgument and @Option, but their longName()s differ");
}
}
OptionModel optionModel = new OptionModel();
optionModel.setLongName(getOptionName(optionArgument.longName(), setter));
char shortName = optionArgument.shortName();
if (shortName != OptionArgument.NO_SHORT) {
if (argumentOptional) {
throw new ModelException("Method " + setter + " is annotated with @OptionArgument and @Option, but in that case a shortName is only allowed on @Option");
}
optionModel.setShortName(shortName);
} else if (argumentOptional) {
shortName = option.shortName();
if (shortName != Option.NO_SHORT) {
optionModel.setShortName(shortName);
}
}
ArgumentModel argumentModel = new ArgumentModel();
Class argumentType = (Class)getSimpleTypeOrCollectionType(setter, OptionArgument.class);
argumentModel.setParser((ArgumentParser)getArgumentParser(setter, argumentType, isSimpleType(setter)));
argumentModel.setToolModel(toolModel);
argumentModel.setType(argumentType);
argumentModel.setMultiplicity(isSimpleType(setter) ? Multiplicity._0_OR_1 : Multiplicity._0_OR_MORE);
argumentModel.setName(optionArgument.argumentName());
argumentModel.setSetter(setter);
optionModel.setArgumentType(argumentOptional ? ArgumentType.OPTIONAL : ArgumentType.REQUIRED);
optionModel.setArgument(argumentModel);
optionModel.getArgument().setOption(optionModel);
return optionModel;
}
private ArgumentModel buildArgument(
final Method setter, Map> orderedArgumentModels) {
Argument argument = setter.getAnnotation(Argument.class);
if (argument == null) {
return null;
}
if (!isSetter(setter)) {
throw new ModelException("Method " + setter + " is annotated with @Argument but is not a setter");
}
if (hasDescription(setter)) {
throw new ModelException(
"Method " + setter + " is annotated with @Argument and @Description: " +
"Arguments should be documented in the class-level @Description");
}
if (isHidden(setter)) {
throw new ModelException(
"Method " + setter + " is annotated with @Argument and @Hidden: " +
"You can't have @Hidden arguments");
}
ArgumentModel argumentModel = new ArgumentModel();
Multiplicity multiplicity = Multiplicity.fromString(argument.multiplicity());
String argumentName = argument.argumentName();
int order = argument.order();
Class argumentType = (Class)getSimpleTypeOrCollectionType(setter, Argument.class);
populateArgumentModel(setter, orderedArgumentModels, argumentModel,
argumentType, multiplicity, argumentName, order);
return argumentModel;
}
private SubtoolModel buildSubtool(
final Method setter, Map> orderedArgumentModels) {
Subtool argument = setter.getAnnotation(Subtool.class);
if (argument == null) {
return null;
}
if (!isSetter(setter)) {
throw new ModelException("Method " + setter + " is annotated with @Subtool but is not a setter");
}
if (hasDescription(setter)) {
throw new ModelException(
"Method " + setter + " is annotated with @Subtool and @Description: " +
"Subtools should be documented in the class-level @Description");
}
if (isHidden(setter)) {
throw new ModelException(
"Method " + setter + " is annotated with @Subtool and @Hidden: " +
"You can't have @Hidden arguments");
}
SubtoolModel argumentModel = new SubtoolModel();
Class argumentType = (Class)setter.getParameterTypes()[0];
Multiplicity multiplicity = Multiplicity._1;
String argumentName = argument.argumentName();
int order = argument.order();
populateArgumentModel(setter, orderedArgumentModels, argumentModel,
argumentType, multiplicity, argumentName, order);
return argumentModel;
}
private void populateArgumentModel(final Method setter,
Map> orderedArgumentModels,
ArgumentModel argumentModel, Class argumentType,
Multiplicity multiplicity, String argumentName, int order) {
argumentModel.setMultiplicity(multiplicity);
argumentModel.setName(argumentName);
argumentModel.setType(argumentType);
final ArgumentParser parser = (ArgumentParser)getArgumentParser(setter, argumentType, !multiplicity.isMultivalued());
if (parser == null) {
throw new ModelException("Unable to parse arguments of " + argumentModel.getType());
}
argumentModel.setParser(parser);
argumentModel.setSetter(setter);
final ArgumentModel> clash = orderedArgumentModels.put(order, argumentModel);
if (clash != null) {
throw new ModelException("Two setters annotated with @Argument with the same order");
}
}
private boolean isHidden(final Method setter) {
return setter.getAnnotation(Hidden.class) != null;
}
private boolean isSimpleType(final Method setter) {
Type t = setter.getGenericParameterTypes()[0];
return t instanceof Class;
}
private Class> getSimpleTypeOrCollectionType(final Method setter, Class extends Annotation> annoType) {
Type t = setter.getGenericParameterTypes()[0];
if (t instanceof Class) {
Class> type = (Class>)t;
if (List.class.isAssignableFrom(type)) {
throw new ModelException("Method " + setter + " is annotated with " + annoType.getSimpleName() + " but the parameter type is a raw List");
}
return (Class>)t;
}
if (!(t instanceof ParameterizedType)) {
// TODO Maybe support wildcards
throw new ModelException("");
}
ParameterizedType pt = (ParameterizedType)t;
Type rt = pt.getRawType();// I.e. List
if (ToolModel.class.equals(rt)) {
return (Class)rt;
}
if (!List.class.equals(rt) && !EnumSet.class.equals(rt)) {
throw new ModelException("Method " + setter + " is annotated with " + annoType.getSimpleName() + " but the parameter type is not java.util.List or java.util.EnumSet");
}
Type ta = pt.getActualTypeArguments()[0];
if (ta instanceof ParameterizedType) {
ta = ((ParameterizedType)ta).getRawType();
}
if (!(ta instanceof Class)) {
throw new ModelException("Method " + setter + " is annotated with " + annoType.getSimpleName() + " but the type parameter to the java.util.List parameter is not a class but a " + ta);
}
Class> argumentType = (Class>)ta;
return argumentType;
}
public abstract String getToolName(String className);
/**
* Returns an iterable of all the tools names known to this tool loader.
*/
public Iterable getToolNames() {
TreeSet result = new TreeSet<>();
for (String className : toolClassNames()) {
result.add(getToolName(className));
}
return result;
}
static interface Handler {
public T handle(String className);
}
/**
* Suggests tool names which are similar to something that was supposed
* to be a tool name, but wasn't.
* @param badlySpelledCommand
* @return A list of tool names which are similar to the give non tool
* name.
*/
public List typo(final String badlySpelledCommand) {
List result = new ArrayList<>();
for (String className : toolClassNames()) {
String toolName = getToolName(className);
if (levenshteinDistance(toolName, badlySpelledCommand) < 3) {
if (loadToolModel(toolName).isPorcelain()) {
result.add(toolName);
}
}
}
return result;
}
protected abstract Iterable toolClassNames();
/* The following two methods taken from wikipedia */
private static int minimum(int a, int b, int c) {
return Math.min(Math.min(a, b), c);
}
private static int levenshteinDistance(CharSequence str1,
CharSequence str2) {
int[][] distance = new int[str1.length() + 1][str2.length() + 1];
for (int i = 0; i <= str1.length(); i++)
distance[i][0] = i;
for (int j = 0; j <= str2.length(); j++)
distance[0][j] = j;
for (int i = 1; i <= str1.length(); i++)
for (int j = 1; j <= str2.length(); j++)
distance[i][j] = minimum(
distance[i - 1][j] + 1,
distance[i][j - 1] + 1,
distance[i - 1][j - 1]
+ ((str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0
: 1));
return distance[str1.length()][str2.length()];
}
public T instance(String toolName, Tool outer) {
String toolClassName = getToolClassName(toolName);
if (toolClassName == null) {
return null;
}
try {
Class toolClass = (Class)Class.forName(toolClassName, false, loader);
return instance(toolClass, outer);
} catch (ReflectiveOperationException e) {
throw new ToolException("Could not instantiate tool class " + toolClassName + " for tool " + toolName, e);
}
}
public T instance(ToolModel toolModel, Tool outer) {
if (!(toolModel instanceof AnnotatedToolModel)) {
return null;
}
AnnotatedToolModel amodel = (AnnotatedToolModel)toolModel;
return instance(amodel.getToolClass(), outer);
}
private T instance(Class toolClass, Tool outer) {
try {
if (toolClass.getEnclosingClass() != null
&& !Modifier.isStatic(toolClass.getModifiers())) {
if (outer == null) {
throw new ToolException("Cannot instantiate non-static innner class without a qualifier");
}
Constructor ctor = toolClass.getConstructor(toolClass.getEnclosingClass());
return ctor.newInstance(outer);
} else {
return toolClass.newInstance();
}
} catch (RuntimeException e) {
throw new ToolException("Could not instantiate tool class " + toolClass.getName(), e);
} catch (ReflectiveOperationException e) {
throw new ToolException("Could not instantiate tool class " + toolClass.getName(), e);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy