-
Notifications
You must be signed in to change notification settings - Fork 418
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();
}
}