All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.redhat.ceylon.common.tool.ToolLoader Maven / Gradle / Ivy

There is a newer version: 1.3.3
Show newest version
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 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 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