这是我的解决方案;也许它对某人有用。基本方法是为每个嵌套的配置文件使用一个新的CommandLine 实例。
以下所有代码 sn-ps 均来自带注释的类;删除了特定于项目的内务管理、错误检查、路径构建等。
问题#1通过parseConfigFile(...)方法解决:
@Option(names = { "configfile", "cf" } )
public void parseConfigfile(final List<String> accumulatedCfgFiles) {
// prune the list, keeping only "new" entries that haven't been seen yet:
List<String> newCfgFiles = this.cfgfileHelper.retainNewOptionValuesOnly(
accumulatedCfgFiles, "configfile");
if(newCfgFiles.isEmpty()) {
// another picoCLI quirk: this happens even if there always are values
return;
} else if(newCfgFiles.size() > 1) {
// loop over the files if you want to allow this, or report an error
}
// some path tinkering left out
File cfgFile = new File(newCfgFiles.get(0));
if(this.stackOfConfigFiles.contains(cfgFile)) {
// report error because of cyclic reference
} else {
this.stackOfConfigFiles.push(cfgFile);
// task a new CommandLine instance with processing that file:
CommandLine cmd = new CommandLine(this);
String[] optionsFromFile = FileUtils.readLines(cfgFile); // Apache Commons
this.cfgfileHelper.wrapParseArgs(cmd, optionsFromFile);
this.stackOfConfigFiles.pop();
}
}
该方法使用NestedCfgfileHelper 的实例(参见下面的源代码),它执行所有嵌套配置特定的内务管理以解决问题#2。每个带注释的类的该帮助器类的一个实例跟踪当前在您的带注释的类上工作的CommandLine 实例。它的构造函数需要助手类应该处理的所有选项的名称:
...
public final NestedCfgfileHelper cfgfileHelper =
new NestedCfgfileHelper(new String[] { "configfile", "section" });
...
以下步骤使所有这些工作:
- 识别那些对“虚假的 setter 方法调用”敏感的选项(大多数不是);
- 如果有,请将
NestedCfgfileHelper 的源代码作为内部类粘贴到带注释的类中;
- 创建一个
NestedCfgfileHelper 的实例作为注释类的公共成员,告诉构造函数所有这些“有问题的”选项的名称;
- 永远不要直接调用
yourInstanceOfCommandLine.parseArgs(...),而是使用instanceOfYourAnnotatedClass.cfgfileHelper.wrapParseArgs(...)将它传递给助手
- 让那些“困难”选项的设置方法...
- ... 首先通过调用
retainNewOptionValuesOnly 来消除先前调用中的“旧”值
- ...然后正常处理剩余的选项值
最后,这是NestedCfgfileHelper的来源:
/** NestedCfgfileHelper ensures that the values of certain options are
* processed just once, despite the picoCLI quirk. */
public final class NestedCfgfileHelper {
/** Maps an (option name|CommandLine instance) pair to the number of
* option values that instance has so far passed for that option.
* Because Java doesn't have Maps with two keys, it's implemented as
* a Map of Maps: */
private Map<String, Map<CommandLine, Integer>> mapOptionAndCLToCount =
new HashMap<>();
/** Constructs a helper instance and prepares it to handle the options
* given as parameters.
*
* @param optionNames any number of Strings, with each String denoting
* one option whose values should be protected against being processed
* multiple times */
public NestedCfgfileHelper(String... optionNames) {
// make one mapping for each option name given:
for(String optionName: optionNames) {
mapOptionAndCLToCount.put(optionName, new HashMap<CommandLine, Integer>());
}
}
/** This stack keeps track of CommandLine instances that are currently
* working on this TFConfig instance. A stack is needed because config files
* can be nested. Entries in the CommandLine stack correspond to entries in
* the config file stack (maintained by the parseConfigfile() method),
* except the lowest (first) one that gets fed the String[] coming directly
* from the main(String[] args) call. */
private Stack<CommandLine> stackOfCmdLineInstances = new Stack<>();
/** Wraps the call to {@link CommandLine#parseArgs(String...)} with some
* housekeeping so that when an annotated setter method is being called
* during option parsing, the helper method can look up from which
* CommandLine instance the call is coming.
* Because parseArg invocations will be nested recursively for nested config
* files, the respective CommandLine instances are kept on a stack.
* @param cl CommandLine instance that's been about to start parsing
* @param args options that are to be parsed */
public void wrapParseArgsCall(final CommandLine cl, final String[] args) {
// the brand new CommandLine instance hasn't passed any values yet,
// so put 0 in all maps:
mapOptionAndCLToCount.forEach(
(String s, Map<CommandLine, Integer> m) -> m.put(cl, 0));
this.stackOfCmdLineInstances.push(cl);
cl.parseArgs(args);
this.stackOfCmdLineInstances.pop();
}
/** This method filters its list parameter, discarding the first n
* entries (assuming they've already been processed), where n is retrieved
* from a Map instance kept for each option name. This method is intended
* to be called by annotated setter methods.
*
* @param accumulated List containing all values (old and new ones
* accumulated) of the option named in the other parameter.
* @param optionName describes the option that's being parsed.
* @return pruned list containing only the "new" values that haven't
* been seen before. */
private List<String> retainNewOptionValuesOnly(
final List<String> accumulated,
final String optionName) {
// get the CommandLine instance currently working on this TFConfig instance:
CommandLine currentCL = this.stackOfCmdLineInstances.peek();
// get the CommandLine->int map for the option name passed:
Map<CommandLine, Integer> map = mapOptionAndCLToCount.get(optionName);
if(map == null) {
throw new IllegalArgumentException("unknown option: " + optionName);
}
/* Find out how many option values it has already passed to the setter.
* For each CL, that number is stored in the map passed to this method.*/
int n = map.get(currentCL);
/* discard the first n entries (they have already been processed) of
* accumulated, keeping only the "new" ones: */
List<String> optionValuesNewThisTime =
accumulated.subList(n, accumulated.size());
// associate the new number of patterns with the current CommandLine:
int newNumber = n + optionValuesNewThisTime.size();
map.put(currentCL, newNumber);
return optionValuesNewThisTime;
}
}