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

com.swirlds.cli.utility.CommandBuilder Maven / Gradle / Ivy

Go to download

Swirlds is a software platform designed to build fully-distributed applications that harness the power of the cloud without servers. Now you can develop applications with fairness in decision making, speed, trust and reliability, at a fraction of the cost of traditional server-based platforms.

There is a newer version: 0.56.6
Show newest version
/*
 * Copyright (C) 2022-2024 Hedera Hashgraph, LLC
 *
 * Licensed 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 com.swirlds.cli.utility;

import com.swirlds.common.formatting.TextEffect;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import picocli.CommandLine;

/**
 * A utility for building command lines automatically from classes annotated with {@link SubcommandOf}.
 */
public final class CommandBuilder {

    private static final CommandLine.Help.ColorScheme COLORFUL_SCHEME = new CommandLine.Help.ColorScheme.Builder()
            .commands(CommandLine.Help.Ansi.Style.bold)
            .options(CommandLine.Help.Ansi.Style.fg_yellow)
            .parameters(CommandLine.Help.Ansi.Style.fg_yellow)
            .optionParams(CommandLine.Help.Ansi.Style.italic)
            .errors(CommandLine.Help.Ansi.Style.fg_red, CommandLine.Help.Ansi.Style.bold)
            .stackTraces(CommandLine.Help.Ansi.Style.italic)
            .build();

    private static final CommandLine.Help.ColorScheme BORING_SCHEME =
            new CommandLine.Help.ColorScheme.Builder().build();

    /**
     * Describes a subcommand.
     *
     * @param subcommandClass
     * 		the class of the subcommand
     * @param parentClass
     * 		the parent command of the subcommand
     */
    public record Subcommand(Class subcommandClass, Class parentClass) {}

    /**
     * Whitelisted packages.
     */
    private static final List whitelistedPackages = new ArrayList<>();

    /**
     * A list of subcommands. Cache the result in a static variable, since we never expect this to change
     * (absent class-loader shenanigans we don't have to worry about supporting).
     */
    private static List subcommands;

    /**
     * A map of subcommand classes to their parent classes. Cache the result in a static variable, since we never
     * expect this to change (absent class-loader shenanigans we don't have to worry about supporting).
     */
    private static Map /* subcommand class */, Class /* parent class */> parentMap;

    private CommandBuilder() {}

    /**
     * Get the current color scheme.
     */
    public static CommandLine.Help.ColorScheme getColorScheme() {
        if (TextEffect.areTextEffectsEnabled()) {
            return COLORFUL_SCHEME;
        } else {
            return BORING_SCHEME;
        }
    }

    /**
     * Walk the class graph and find all classes that implement {@link SubcommandOf}.
     *
     * @return a list of all classes annotated as sub-commands
     */
    private static synchronized List findSubcommands() {

        if (subcommands != null) {
            return subcommands;
        }

        final ClassGraph classGraph = new ClassGraph()
                .enableClassInfo()
                .enableAnnotationInfo()
                .whitelistPackages(whitelistedPackages.toArray(new String[0]));

        final List list = new ArrayList<>();

        try (final ScanResult scanResult = classGraph.scan(Runtime.getRuntime().availableProcessors())) {
            for (final ClassInfo classInfo : scanResult.getClassesWithAnnotation(SubcommandOf.class.getName())) {
                final Class subcommandClass = classInfo.loadClass();
                final SubcommandOf subcommandOf = (SubcommandOf) classInfo
                        .getAnnotationInfo(SubcommandOf.class.getName())
                        .loadClassAndInstantiate();
                final Class parentClass = subcommandOf.value();

                list.add(new Subcommand(subcommandClass, parentClass));
            }
        }

        subcommands = Collections.unmodifiableList(list);
        return subcommands;
    }

    /**
     * Build a map of subcommands to their parents.
     *
     * @return a map of subcommands to their parents
     */
    private static synchronized Map /* subcommand class */, Class /* parent class */> buildParentMap() {
        if (parentMap != null) {
            return parentMap;
        }

        final Map, Class> map = new HashMap<>();
        subcommands.forEach(subcommand -> map.put(subcommand.subcommandClass(), subcommand.parentClass()));

        parentMap = Collections.unmodifiableMap(map);
        return parentMap;
    }

    /**
     * Build a new command line with the appropriate color scheme.
     */
    private static CommandLine buildCommandLineWithColorScheme(final Class clazz) {
        final CommandLine commandLine = new CommandLine(clazz);
        commandLine.setColorScheme(getColorScheme());
        return commandLine;
    }

    /**
     * Build command lines for all commands/subcommands and return a map between
     * each class and its associated command line object.
     *
     * @return a map from command class to corresponding CommandLine object
     */
    private static Map /* subcommand class */, CommandLine /* subcommand CommandLine */> buildCommandLines() {
        final Map, CommandLine> map = new HashMap<>();

        for (final Subcommand subcommand : findSubcommands()) {
            map.computeIfAbsent(subcommand.parentClass(), CommandBuilder::buildCommandLineWithColorScheme);
        }

        findSubcommands()
                .forEach(subcommand -> map.put(
                        subcommand.subcommandClass(), buildCommandLineWithColorScheme(subcommand.subcommandClass())));
        return map;
    }

    /**
     * Link all subcommands with their parent command lines.
     *
     * @param commandLineMap
     * 		a map of command to corresponding CommandLine object
     */
    private static void linkCommandLines(
            Map /* subcommand class */, CommandLine /* subcommand CommandLine */> commandLineMap) {

        final Map, Class> pMap = buildParentMap();

        pMap.keySet().stream().sorted(Comparator.comparing(Class::toString)).forEachOrdered(command -> {
            final CommandLine parentCommandLine = commandLineMap.get(pMap.get(command));
            if (parentCommandLine == null) {
                return;
            }

            parentCommandLine.addSubcommand(commandLineMap.get(command));
        });
    }

    /**
     * Add a whitelisted package, used when scanning the class graph for CLI commands. Must be called before
     * the first call to {@link #buildCommandLine(Class)}.
     *
     * @param packagePrefix
     * 		a package prefix to whitelist
     */
    public static synchronized void whitelistCliPackage(final String packagePrefix) {
        if (subcommands != null) {
            throw new IllegalStateException("Cannot whitelist package after subcommands have been found");
        }
        whitelistedPackages.add(packagePrefix);
    }

    /**
     * Walk the class graph and register any and all subcommands.
     */
    public static CommandLine buildCommandLine(final Class rootCommandClass) {
        final Map, CommandLine> commandLineMap = buildCommandLines();
        linkCommandLines(commandLineMap);
        final CommandLine commandLine = commandLineMap.get(rootCommandClass);
        if (commandLine == null) {
            throw new IllegalStateException("No command line found for " + rootCommandClass);
        }
        return commandLineMap.get(rootCommandClass);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy