ballerina-platform / ballerina-lang

The Ballerina Programming Language
https://ballerina.io/
Apache License 2.0
3.55k stars 735 forks source link

[Improvement]: Validating the tool and its sub-command existence during the tool creation instead #42460

Open Thevakumar-Luheerathan opened 3 months ago

Thevakumar-Luheerathan commented 3 months ago

Description

The above validation is needed for the following scenarios. There may be more scenarios as well.; 1) A tool can be mistakenly packed without a main command. And, it can be pushed to the Central. Tool is not found error is shown while using the tool.

2) We need to specify commands, and subcommands of a tool in io.ballerina.projects.buildtools.CodeGeneratorTool file. If the actual commands do not match with the entries in the file, a bad-sad error is thrown as follows during the usage.

We appreciate it if you can report the code that broke Ballerina in
https://github.com/ballerina-platform/ballerina-lang/issues with the
log you get below and your sample code.

We thank you for helping make us better.

[2024-04-01 14:02:46,932] SEVERE {b7a.log.crash} - io.ballerina.projects.buildtools.CodeGeneratorTool: Provider build.tool.runner.SampleToolMainCmd not found 
java.util.ServiceConfigurationError: io.ballerina.projects.buildtools.CodeGeneratorTool: Provider build.tool.runner.SampleToolMainCmd not found
        at java.base/java.util.ServiceLoader.fail(ServiceLoader.java:593)
        at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.nextProviderClass(ServiceLoader.java:1219)
        at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNextService(ServiceLoader.java:1228)
        at java.base/java.util.ServiceLoader$LazyClassPathLookupIterator.hasNext(ServiceLoader.java:1273)
        at java.base/java.util.ServiceLoader$2.hasNext(ServiceLoader.java:1309)
        at java.base/java.util.ServiceLoader$ProviderSpliterator.tryAdvance(ServiceLoader.java:1491)
        at java.base/java.util.Spliterator.forEachRemaining(Spliterator.java:332)
        at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
        at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
        at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:575)
        at java.base/java.util.stream.AbstractPipeline.evaluateToArrayNode(AbstractPipeline.java:260)
        at java.base/java.util.stream.ReferencePipeline.toArray(ReferencePipeline.java:616)
        at io.ballerina.projects.util.BuildToolUtils.getTargetTool(BuildToolUtils.java:96)
        at io.ballerina.cli.task.RunBuildToolsTask.execute(RunBuildToolsTask.java:146)
        at io.ballerina.cli.TaskExecutor.executeTasks(TaskExecutor.java:40)
        at io.ballerina.cli.cmd.BuildCommand.execute(BuildCommand.java:291)
        at java.base/java.util.Optional.ifPresent(Optional.java:178)
        at io.ballerina.cli.launcher.Main.main(Main.java:58)

It is better if we can validate a tool while packing.

Describe your problem(s)

No response

Describe your solution(s)

No response

Related area

-> Compilation

Related issue(s) (optional)

No response

Suggested label(s) (optional)

No response

Suggested assignee(s) (optional)

No response

Xenowa commented 2 months ago

Adding a tool validation logic (validateTool method) to the PackCommand can resolve this issue.

package io.ballerina.cli.cmd;

public class PackCommand implements BLauncherCmd {
    // ...

    @Override
    public void execute() {
        // ...

        // Validate tool and sub-command existence
        try {
            validateTool(project);
        }catch (ServiceConfigurationError | IOException e) {
            CommandUtil.printError(this.errStream, e.getMessage(), null, false);
            CommandUtil.exitError(this.exitWhenFinish);
            return;
        }

        TaskExecutor taskExecutor = new TaskExecutor.TaskBuilder()
        // ...
    }

    private void validateTool(Project project) throws ServiceConfigurationError, IOException {
        // Get the BalTools.toml content
        Optional<BalToolDescriptor> balToolDescriptor = project.currentPackage().manifest().balToolDescriptor();
        if(balToolDescriptor.isPresent()){
            BalToolDescriptor balToolManifest = balToolDescriptor.get();
            String id = balToolManifest.tool().getId();
            List<URL> jarURLs = new ArrayList<>();

            List<String> toolDependencyJARs = balToolManifest.getBalToolDependencies();
            toolDependencyJARs.forEach(dependencyJAR ->{
                Path jarPath = Path.of(dependencyJAR);
                if(Files.exists(jarPath)){
                    // Convert the paths to the URL Format
                    try {
                        jarURLs.add(jarPath.toUri().toURL());
                    } catch (MalformedURLException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            URLClassLoader ucl = new URLClassLoader(jarURLs.toArray(new URL[0]));

            // Validate BLauncherCmd JARs
            validateBlauncherTool(id, ucl, jarURLs);

            // Validate CodeGeneratorTool JARs
            validateCodeGeneratorTool(id, ucl, jarURLs);
        }
    }

    private void validateBlauncherTool(String toolId,URLClassLoader ucl, List<URL> jarURLs){
        List<String> allCommandNames = new ArrayList<>();
        List<String> subCommandNames = new ArrayList<>();

        // Load and check class which implements BLauncherCmd interface
        ServiceLoader<BLauncherCmd> customBlauncherCmds = ServiceLoader.load(BLauncherCmd.class, ucl);
        customBlauncherCmds.forEach(customCmd ->{
            if(jarURLs.contains(customCmd.getClass().getProtectionDomain().getCodeSource().getLocation())){
                // Retrieve annotation of the command
                CommandLine.Command commandAnnotation = customCmd.getClass()
                        .getAnnotation(CommandLine.Command.class);

                // Validate if main command follows expected pattern
                boolean mainCommandValidity = validateCommandName(commandAnnotation.name());
                if(!mainCommandValidity){
                    CommandUtil.printError(this.errStream, "invalid command name format for command '" +
                            commandAnnotation.name() + "'",null, false);
                    CommandUtil.exitError(this.exitWhenFinish);
                }
                allCommandNames.add(commandAnnotation.name());

                // Retrieve the sub commands
                Class<?>[] subcommands = commandAnnotation.subcommands();
                List<Class<?>> list = Arrays.stream(subcommands).toList();
                list.forEach(subcommand ->{
                    // Retrieve annotation of the command
                    Class<? extends BLauncherCmd> subCmdClass = (Class<? extends BLauncherCmd>) subcommand;
                    CommandLine.Command subCommandAnnotation = subCmdClass.getAnnotation(CommandLine.Command.class);

                    // Validate if sub command follows expected pattern
                    boolean subCommandValidity = validateCommandName(subCommandAnnotation.name());
                    if(!subCommandValidity){
                        CommandUtil.printError(this.errStream, "invalid command name format for command '" +
                                        subCommandAnnotation.name() + "'",null, false);
                        CommandUtil.exitError(this.exitWhenFinish);
                    }
                    subCommandNames.add(subCommandAnnotation.name());
                });
            }
        });

        if(toolId != null){
            allCommandNames.forEach(commandName ->{
                if(!subCommandNames.contains(commandName) && !toolId.equals(commandName)){
                    CommandUtil.printError(this.errStream, "command name '" + commandName +
                            "' does not match id '" + toolId + "' provided in " + ProjectConstants.BAL_TOOL_TOML,
                            null, false);
                    CommandUtil.exitError(this.exitWhenFinish);
                }
            });
        }
    }

    private void validateCodeGeneratorTool(String toolId,URLClassLoader ucl, List<URL> jarURLs){
        List<String> allCommandNames = new ArrayList<>();
        List<String> subCommandNames = new ArrayList<>();

        // Load and check class which implements CodeGeneratorTool interface
        ServiceLoader<CodeGeneratorTool> customBlauncherCmds = ServiceLoader.load(CodeGeneratorTool.class, ucl);
        customBlauncherCmds.forEach(customCmd ->{
            if(jarURLs.contains(customCmd.getClass().getProtectionDomain().getCodeSource().getLocation())){
                // Retrieve annotation of the command
                ToolConfig commandAnnotation = customCmd.getClass().getAnnotation(ToolConfig.class);

                // Validate if main command follows expected pattern
                boolean mainCommandValidity = validateCommandName(commandAnnotation.name());
                if(!mainCommandValidity){
                    CommandUtil.printError(this.errStream, "invalid command name format for command '" +
                            commandAnnotation.name() + "'",null, false);
                    CommandUtil.exitError(this.exitWhenFinish);
                }
                allCommandNames.add(commandAnnotation.name());

                // Retrieve the sub commands
                Class<?>[] subcommands = commandAnnotation.subcommands();
                List<Class<?>> list = Arrays.stream(subcommands).toList();
                list.forEach(subcommand ->{
                    // Retrieve annotation of the command
                    Class<? extends CodeGeneratorTool> subCmdClass = (Class<? extends CodeGeneratorTool>) subcommand;
                    ToolConfig subCommandAnnotation = subCmdClass.getAnnotation(ToolConfig.class);

                    // Validate if sub command follows expected pattern
                    boolean subCommandValidity = validateCommandName(subCommandAnnotation.name());
                    if(!subCommandValidity){
                        CommandUtil.printError(this.errStream, "invalid command name format for command '" +
                                subCommandAnnotation.name() + "'",null, false);
                        CommandUtil.exitError(this.exitWhenFinish);
                    }
                    subCommandNames.add(subCommandAnnotation.name());
                });
            }
        });

        if(toolId != null){
            allCommandNames.forEach(commandName ->{
                if(!subCommandNames.contains(commandName) && !toolId.equals(commandName)){
                    CommandUtil.printError(this.errStream, "command name '" + commandName +
                                    "' does not match id '" + toolId + "' provided in " + ProjectConstants.BAL_TOOL_TOML,
                            null, false);
                    CommandUtil.exitError(this.exitWhenFinish);
                }
            });
        }
    }

    private boolean validateCommandName(String cmdName) {
        boolean commandValidity = true;
        if (cmdName == null || cmdName.isEmpty()) {
            commandValidity = false;
        } else if (!cmdName.matches("^\\w+$")) {
            commandValidity = false;
        } else if (cmdName.startsWith("_")) {
            commandValidity = false;
        } else if (cmdName.endsWith("_")) {
            commandValidity = false;
        } else if (cmdName.contains("__")) {
            commandValidity = false;
        }
        return commandValidity;
    }
}

For testing the solution: