Skip to content

Commit

Permalink
[#1706] Resolve messages against parent resource bundles with updated…
Browse files Browse the repository at this point in the history
… i18n unit tests and fixed whitespace.
  • Loading branch information
rsenden authored and remkop committed Jun 28, 2022
1 parent 47a3202 commit 295b3d3
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 19 deletions.
51 changes: 40 additions & 11 deletions src/main/java/picocli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import picocli.CommandLine.Help.Ansi.IStyle;
import picocli.CommandLine.Help.Ansi.Style;
import picocli.CommandLine.Help.Ansi.Text;
Expand Down Expand Up @@ -11559,6 +11560,7 @@ public static class Messages {
private final String bundleBaseName;
private final ResourceBundle rb;
private final Set<String> keys;
private Messages parent;
public Messages(CommandSpec spec, String baseName) {
this(spec, baseName, createBundle(baseName));
}
Expand All @@ -11582,6 +11584,21 @@ private static String extractName(ResourceBundle rb) {
return (String) ResourceBundle.class.getDeclaredMethod("getBaseBundleName").invoke(rb);
} catch (Exception ignored) { return "?"; }
}
public Messages parent() {
CommandSpec parentSpec = this.spec.parent();
if (parent == null || parent.spec != parentSpec) {
parent = null; // Refresh if parentSpec doesn't match
while (parent == null && parentSpec != null) {
String parentResourceBundleBaseName = parentSpec.resourceBundleBaseName();
if (parentResourceBundleBaseName != null && !parentResourceBundleBaseName.equals(this.bundleBaseName)) {
parent = new Messages(parentSpec, parentResourceBundleBaseName);
} else {
parentSpec = parentSpec.parent();
}
}
}
return parent;
}
private static Set<String> keys(ResourceBundle rb) {
if (rb == null) { return Collections.emptySet(); }
Set<String> keys = new LinkedHashSet<String>();
Expand All @@ -11597,8 +11614,8 @@ private static Set<String> keys(ResourceBundle rb) {
public static Messages copy(CommandSpec spec, Messages original) {
return original == null ? null : new Messages(spec, original.bundleBaseName, original.rb);
}
/** Returns {@code true} if the specified {@code Messages} is {@code null} or has a {@code null ResourceBundle}. */
public static boolean empty(Messages messages) { return messages == null || messages.rb == null; }
/** Returns {@code true} if the specified {@code Messages} is {@code null}, has a {@code null ResourceBundle}, or has a {@code null parent Messages}. */
public static boolean empty(Messages messages) { return messages == null || messages.isEmpty(); }

/** Returns the String value found in the resource bundle for the specified key, or the specified default value if not found.
* @param key unqualified resource bundle key. This method will first try to find a value by qualifying the key with the command's fully qualified name,
Expand All @@ -11609,12 +11626,19 @@ public static Messages copy(CommandSpec spec, Messages original) {
public String getString(String key, String defaultValue) {
if (isEmpty()) { return defaultValue; }
String cmd = spec.qualifiedName(".");
if (keys.contains(cmd + "." + key)) { return rb.getString(cmd + "." + key); }
if (keys.contains(key)) { return rb.getString(key); }
return defaultValue;
String qualifiedKey = cmd + "." + key;
String result = getStringForExactKey(qualifiedKey);
if (result == null) { result = getStringForExactKey(key); }
return result != null ? result : defaultValue;
}

boolean isEmpty() { return rb == null || keys.isEmpty(); }
private String getStringForExactKey(String key) {
if (keys.contains(key)) { return rb.getString(key); }
else if (parent() != null) { return parent().getStringForExactKey(key); }
else { return null; }
}

boolean isEmpty() { return (rb == null || keys.isEmpty()) && (parent() == null || parent().isEmpty()); }

/** Returns the String array value found in the resource bundle for the specified key, or the specified default value if not found.
* Multi-line strings can be specified in the resource bundle with {@code key.0}, {@code key.1}, {@code key.2}, etc.
Expand All @@ -11626,10 +11650,15 @@ public String getString(String key, String defaultValue) {
public String[] getStringArray(String key, String[] defaultValues) {
if (isEmpty()) { return defaultValues; }
String cmd = spec.qualifiedName(".");
List<String> result = addAllWithPrefix(rb, cmd + "." + key, keys, new ArrayList<String>());
if (!result.isEmpty()) { return result.toArray(new String[0]); }
addAllWithPrefix(rb, key, keys, result);
return result.isEmpty() ? defaultValues : result.toArray(new String[0]);
String qualifiedKey = cmd + "." + key;
String[] result = getStringArrayForExactKey(qualifiedKey);
if (result == null) { result = getStringArrayForExactKey(key); }
return result != null ? result : defaultValues;
}
private String[] getStringArrayForExactKey(String key) {
List<String> result = addAllWithPrefix(rb, key, keys, new ArrayList<String>());
if (!result.isEmpty()) { return result.toArray(new String[0]); }
return parent() == null ? null : parent().getStringArrayForExactKey(key);
}
private static List<String> addAllWithPrefix(ResourceBundle rb, String key, Set<String> keys, List<String> result) {
if (keys.contains(key)) { result.add(rb.getString(key)); }
Expand Down Expand Up @@ -18237,7 +18266,7 @@ private Tracer() {}
/** Returns whether the current trace level is WARN or higher. */
public boolean isWarn() { return level.isEnabled(TraceLevel.WARN); }
/** Returns whether the current trace level is OFF (the lowest). */
public boolean isOff() { return level== TraceLevel.OFF; }
public boolean isOff() { return level == TraceLevel.OFF; }
/** Prints the specified message if the current trace level is WARN or higher.
* @param msg the message to print; may use {@link String#format(String, Object...)} syntax
* @param params Arguments referenced by the format specifiers in the format string. If there are more arguments than format specifiers, the extra arguments are ignored. The number of arguments is variable and may be zero.
Expand Down
10 changes: 9 additions & 1 deletion src/test/java/picocli/I18nCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import picocli.CommandLine.Mixin;

@Command(name = "i18n-top",
resourceBundle = "picocli.SharedMessages",
subcommands = {CommandLine.HelpCommand.class, I18nSubcommand.class },
subcommands = {CommandLine.HelpCommand.class, I18nSubcommand.class, I18nSubcommand2.class },
mixinStandardHelpOptions = true,
description = {"top desc 1", "top desc 2", "top desc 3"},
descriptionHeading = "top desc heading%n",
Expand All @@ -33,6 +34,13 @@ public class I18nCommand {
@Parameters(index = "1", description = "top param1 description")
String param1;

@Option(
names = {"--optionWithDescriptionFromParent"},
descriptionKey = "optionWithDescriptionFromParent",
scope = CommandLine.ScopeType.INHERIT
)
public boolean parentOption1;

@Override
public String toString() {
return getClass().getName();
Expand Down
30 changes: 30 additions & 0 deletions src/test/java/picocli/I18nSubcommand2.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package picocli;

import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Mixin;

/**
* This command was created to test Issue #1706.
* We are testing here to ensure that the description for a shared Mixin option, where the description for that option
* is defined in a parent command's resource bundle, can still be displayed correctly by a subcommand that's using a
* resource bundle that doesn't have that Mixin's option description defined. Ideally, if the subcommand's resource
* bundle doesn't have the needed description key, picocli SHOULD check the resource bundle of the parent command to see
* if the description key exists.
*/
@Command(name = "sub2",
subcommands = CommandLine.HelpCommand.class,
mixinStandardHelpOptions = true,
resourceBundle = "picocli.ResourceBundlePropagationTest")
public class I18nSubcommand2 {
@Option(names = {"-a", "--aaa"}, descriptionKey = "additional.value")
String a;

// There's an option, --optionWithDescriptionFromParent, which is inherited from the parent command with description

@Option(names = {"-x"}) // Description should come from Parent command's resource bundle
String x;

@Option(names = {"-q"}) // Description is in both parent & sub command resource bundles, but subcommand's should take priority
String q;
}
Loading

0 comments on commit 295b3d3

Please sign in to comment.