Skip to content

zzz 1681 resourcebundle

Remko Popma edited this page Jul 15, 2022 · 1 revision
package picocli.codegen;

import picocli.CommandLine;
import picocli.CommandLine.Mixin;
import picocli.CommandLine.Model.CommandSpec;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Spec;
import picocli.codegen.util.Assert;
import picocli.codegen.util.Util;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Callable;

/**
 * Command that generates a resource bundle for the specified command hierarchy.
 * @since 4.7
 */
public class ResourceBundleGenerator {

    static class Config {

        @Option(names = {"-c", "--charset"}, defaultValue = "ISO-8859-1", paramLabel = "<charset>",
            description = {"Character encoding to use when writing resource bundle properties files. " +
                "Default: ${DEFAULT-VALUE}. Use UTF-8 for Java 9 and later, ISO-8859-1 for Java 8 and older."})
        public Charset charset;

        @Option(names = {"-d", "--outdir"}, defaultValue = ".", paramLabel = "<outdir>",
            description = {"Output directory to write the generated resource bundle properties files to. " +
                "If not specified, files are written to the current directory."})
        File directory;

        @Option(names = {"-v", "--verbose"},
            description = {
                "Specify multiple -v options to increase verbosity.",
                "For example, `-v -v -v` or `-vvv`"})
        boolean[] verbosity = new boolean[0];

        @Option(names = {"-f", "--force"}, negatable = true,
            description = { "Overwrite existing files. " +
                "The default is `--no-force`, meaning processing is aborted and the process exits " +
                "with status code 4 if a file already exists."})
        boolean force;

        private void verbose(String message, Object... params) {
            if (verbosity.length > 0) {
                System.err.printf(message, params);
            }
        }

        private void verboseDetailed(String message, Object... params) {
            if (verbosity.length > 1) {
                System.err.printf(message, params);
            }
        }
    }

    @CommandLine.Command(name = "generate-resourcebundle",
        version = "${COMMAND-FULL-NAME} " + CommandLine.VERSION,
        showAtFileInUsageHelp = true,
        mixinStandardHelpOptions = true, sortOptions = false, usageHelpAutoWidth = true, usageHelpWidth = 100,
        description = {"Generates one or more resource bundle properties files in the specified directory."},
        exitCodeListHeading = "%nExit Codes (if enabled with `--exit`)%n",
        exitCodeList = {
            "0:Successful program execution.",
            "1:A runtime exception occurred while generating resource bundles.",
            "2:Usage error: user input for the command was incorrect, " +
                "e.g., the wrong number of arguments, a bad flag, " +
                "a bad syntax in a parameter, etc.",
            "4:Target file exists in the output directory. (Remove the file or use `--force` to overwrite.)"
        },
        footer = {
            "Example",
            "-------",
            "  java -cp \"myapp.jar;picocli-4.7.0-SNAPSHOT.jar;picocli-codegen-4.7.0-SNAPSHOT.jar\" " +
                "picocli.codegen.ResourceBundleGenerator my.pkg.MyClass"
        }
    )
    private static class App implements Callable<Integer> {

        @Parameters(arity = "1..*", description = "One or more command classes to generate man pages for.")
        Class<?>[] classes = new Class<?>[0];

        @Mixin Config config;
        @Spec CommandSpec spec;

        @Option(names = {"-c", "--factory"}, description = "Optionally specify the fully qualified class name of the custom factory to use to instantiate the command class. " +
            "If omitted, the default picocli factory is used.")
        String factoryClass;

        @Option(names = "--exit", negatable = true,
            description = "Specify `--exit` if you want the application to call `System.exit` when finished. " +
                "By default, `System.exit` is not called.")
        boolean exit;

        public Integer call() throws Exception {
            List<CommandSpec> specs = Util.getCommandSpecs(factoryClass, classes);
            return generateResources(config, specs.toArray(new CommandSpec[0]));
        }
    }

    /**
     * Invokes {@link #generateResources(Config, CommandSpec...)} to generate resource bundle properties files for
     * the user-specified {@code @Command}-annotated classes.
     * <p>
     *     If the {@code --exit} option is specified, {@code System.exit} is invoked
     *     afterwards with an exit code as follows:
     * </p>
     * <ul>
     * <li>0: Successful program execution.</li>
     * <li>1: A runtime exception occurred while generating man pages.</li>
     * <li>2: Usage error: user input for the command was incorrect,
     *        e.g., the wrong number of arguments, a bad flag,
     *        a bad syntax in a parameter, etc.</li>
     * <li>4: A target file exists in the destination directory. (Remove the file or use `--force` to overwrite.)</li>
     * </ul>
     * @param args command line arguments to be parsed. Must include the classes to
     *             generate files for.
     */
    public static void main(String[] args) {
        App app = new App();
        int exitCode = new CommandLine(app).execute(args);
        if (app.exit) {
            System.exit(exitCode);
        }
    }

    /**
     * Generates resource bundle properties files for the specified classes to the specified output directory.
     * @param outdir Output directory to write the generated resource bundle properties files to.
     * @param verbosity the length of this array determines verbosity during processing
     * @param overwrite Overwrite existing resource bundle files.
     *                         The default is false, meaning processing is aborted and the process exits
     *                         with status code 4 if a target file already exists.
     * @param specs the Commands to generate resource bundle properties files for
     * @return the exit code
     * @throws IOException if a problem occurred writing to the file system
     */
    public static int generateResources(File outdir,
                                      Charset charset,
                                      boolean[] verbosity,
                                      boolean overwrite,
                                      CommandSpec... specs) throws IOException {
        Config config = new Config();
        config.directory = outdir;
        config.charset = charset;
        config.verbosity = verbosity;
        config.force = overwrite;

        return generateResources(config, specs);
    }

    static int generateResources(Config config, CommandSpec... specs) throws IOException {
        Assert.notNull(config, "config");
        Assert.notNull(config.directory, "output directory");
        Assert.notNull(config.charset, "charset");
        Assert.notNull(config.verbosity, "verbosity array");

        traceAllSpecs(specs, config);

        for (CommandSpec spec : specs) {
            int result = generateSingleResourceBundle(config, spec);
            if (result != CommandLine.ExitCode.OK) {
                return result;
            }

            Set<CommandSpec> done = new HashSet<CommandSpec>();

            // recursively create man pages for subcommands
            for (CommandLine sub : spec.subcommands().values()) {
                CommandSpec subSpec = sub.getCommandSpec();
                if (done.contains(subSpec) || subSpec.usageMessage().hidden()) {continue;}
                done.add(subSpec);
                result = generateResources(config, subSpec);
                if (result != CommandLine.ExitCode.OK) {
                    return result;
                }
            }
        }
        return CommandLine.ExitCode.OK;
    }

    private static void traceAllSpecs(CommandSpec[] specs, Config config) {
        List<String> all = new ArrayList<String>();
        for (CommandSpec spec: specs) {
            Object obj = spec.userObject();
            if (obj == null) {
                all.add(spec.name() + " (no user object)");
            } else if (obj instanceof Method) {
                all.add(spec.name() + " (" + ((Method) obj).toGenericString() + ")");
            } else {
                all.add(obj.getClass().getName());
            }
        }
        config.verbose("Generating resource bundles for %s and all subcommands%n", all);
    }

    private static int generateSingleResourceBundle(Config config, CommandSpec spec) throws IOException {
        if (!mkdirs(config, config.directory)) {
            return CommandLine.ExitCode.SOFTWARE;
        }
        File manpage = new File(config.directory, makeFileName(spec));
        config.verbose("Generating man page %s%n", manpage);

        return generateSingleResourceBundle(spec, manpage, config.charset);
    }

    private static boolean mkdirs(Config config, File directory) {
        if (directory != null && !directory.exists()) {
            config.verboseDetailed("Creating directory %s%n", directory);

            if (!directory.mkdirs()) {
                System.err.println("Unable to mkdirs for " + directory.getAbsolutePath());
                return false;
            }
        }
        return true;
    }

    private static String makeFileName(CommandSpec spec) {
        return (spec.userObject().getClass().getName() + "Resources.properties");
    }

    private static int generateSingleResourceBundle(CommandSpec spec, File outfile, Charset charset) throws IOException {
        OutputStreamWriter writer = null;
        PrintWriter pw = null;
        try {
            writer = new OutputStreamWriter(new FileOutputStream(outfile), charset);
            pw = new PrintWriter(writer);
            //writeSingleResourceBundle(pw, spec, charset); // FIXME // TODO
        } finally {
            Util.closeSilently(pw);
            Util.closeSilently(writer);
        }
        return CommandLine.ExitCode.OK;
    }

    private static String escape(String src, Charset charset) {
        final CharsetEncoder encoder = charset.newEncoder();
        return escape(src, encoder);
    }

    private static String escape(String src, CharsetEncoder encoder) {
        final StringBuilder result = new StringBuilder();
        for (final Character character : src.toCharArray()) {
            if (encoder.canEncode(character)) {
                result.append(character);
            } else {
                result.append("\\u");
                result.append(Integer.toHexString(0x10000 | character).substring(1).toUpperCase());
            }
        }
        return result.toString();
    }
}