/*
* This file is part of JOP, the Java Optimized Processor
* see <http://www.jopdesign.com/>
*
* Copyright (C) 2010, Stefan Hepp (stefan@stefant.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.jopdesign.common.config;
import com.jopdesign.common.config.Config.BadConfigurationError;
import com.jopdesign.common.config.Config.BadConfigurationException;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Stefan Hepp (stefan@stefant.org)
*/
public class OptionGroup {
public static final String CMD_KEY = "cmd";
private OptionChecker checker;
private Config config;
private String prefix;
/**
* List of all non-hidden options in this group.
*/
private List<Option<?>> availableOptions;
/**
* Map of options to their respective keys (excluding group prefix).
*/
private Map<String, Option<?>> optionSet;
/**
* Map of subgroups and their OptionGroups.
*/
private Map<String, OptionGroup> subGroups;
/**
* Map of option keys (relative to this group) to a set of option keys (global) which must be enabled
* to make an option in use. If an option is not in this map, it is always used.
*/
private Map<String, Set<String>> enableOptions;
/**
* Set of all subgroup names which are handled as commands
*/
private Set<String> commands;
public OptionGroup(Config config) {
this(config, null);
}
public OptionGroup(Config config, String prefix) {
this.config = config;
this.prefix = prefix;
availableOptions = new LinkedList<Option<?>>();
optionSet = new HashMap<String, Option<?>>();
subGroups = new HashMap<String, OptionGroup>(1);
commands = new HashSet<String>(0);
enableOptions = new HashMap<String, Set<String>>();
}
public Config getConfig() {
return config;
}
public OptionChecker getChecker() {
return checker;
}
public void setChecker(OptionChecker checker) {
this.checker = checker;
}
public String getPrefix() {
return prefix;
}
public Set<String> availableSubgroups() {
return subGroups.keySet();
}
public boolean hasCommands() {
return commands.size() > 0;
}
public OptionGroup addGroup(String prefix, boolean isCommand) {
OptionGroup grp = new OptionGroup(config, prefix);
subGroups.put(prefix, grp);
if (isCommand) commands.add(prefix);
return grp;
}
/**
* Get an existing group, or if it does not exist, create a new one.
* @param group the name of the subgroup
* @return a subgroup by that name.
*/
public OptionGroup getGroup(String group) {
OptionGroup og = subGroups.get(group);
if (og == null) {
og = addGroup(group, false);
}
return og;
}
public String selectedCommand() {
return config.getValue(getConfigKey(CMD_KEY));
}
public Option findOption(String subKey) {
int pos = subKey.indexOf('.');
if (pos == -1) {
return getOptionSpec(subKey);
}
OptionGroup group = subGroups.get(subKey.substring(0,pos));
if (group == null) {
return getOptionSpec(subKey);
}
return group.findOption(subKey.substring(pos+1));
}
public OptionGroup findOptionGroup(String subKey) {
int pos = subKey.indexOf('.');
if (pos == -1) {
// check if the key specifies a group or a key: if the group is not known, assume its an option key
if (subGroups.containsKey(subKey)) {
return subGroups.get(subKey);
} else {
return this;
}
}
OptionGroup group = subGroups.get(subKey.substring(0,pos));
if (group == null) {
// no subgroup by that key, return this group
return this;
}
return group.findOptionGroup(subKey.substring(pos + 1));
}
/*
* Setup Options
* ~~~~~~~~~~~~~
*/
public List<Option<?>> availableOptions() {
return availableOptions;
}
public Collection<Option<?>> getOptions() {
return optionSet.values();
}
public void addOption(Option option) {
addOption(option, true);
}
public void addOption(Option option, boolean available) {
if (optionSet.containsKey(option.getKey())) {
for (Iterator<Option<?>> it = availableOptions.iterator(); it.hasNext();) {
Option opt = it.next();
if (opt.getKey().equals(option.getKey())) {
it.remove();
break;
}
}
}
optionSet.put(option.getKey(), option);
// we keep the options in an additional list to have them sorted in the same way they are added.
if (available) availableOptions.add(option);
addEnableOptions(option.getKey());
}
private void addEnableOptions(String key) {
String enable = config.getEnableOption();
Set<String> options = enableOptions.get(key);
if (enable == null && options != null) {
// option is used outside tool too, so it is always enabled
enableOptions.remove(key);
return;
}
if (enable != null) {
if (options == null) {
options = new HashSet<String>(1);
enableOptions.put(key, options);
}
options.add(enable);
}
}
public void addOptions(Option[][] options) {
for (Option[] optList : options) {
addOptions(optList);
}
}
public void addOptions(Option[] options) {
for (Option opt : options) {
addOption(opt);
}
}
public void addOptions(Option[] options, boolean available) {
for (Option opt : options) {
addOption(opt, available);
}
}
public Option getShortOptionKey(char shortKey) {
if (shortKey == Option.SHORT_NONE) {
return null;
}
for (Option o : getOptions()) {
if (o.getShortKey() == shortKey) {
return o;
}
}
return null;
}
public Option getOptionSpec(String key) {
return optionSet.get(key);
}
public boolean containsOption(Option option) {
return optionSet.containsKey(option.getKey());
}
public boolean containsOption(String key) {
return optionSet.containsKey(key);
}
public String getConfigKey(Option<?> option) {
return getConfigKey(option.getKey());
}
public String getConfigKey(String key) {
return prefix == null ? key : prefix + '.' + key;
}
/*
* Access Option Values
* ~~~~~~~~~~~~~~~~~~~~
*/
public boolean isHidden(Option option) {
return !availableOptions.contains(option);
}
/**
* Check if an option is used, i.e. if the options which must be set to enable this option are set.
*
* @param option the option in this group to check.
* @return true if the option is used/enabled.
*/
public boolean isUsed(Option option) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
Set<String> keys = enableOptions.get(option.getKey());
if (keys == null) {
return true;
}
for (String key : keys) {
if (config.isEnabled(key)) {
return true;
}
}
return false;
}
/**
* Check if option has been assigned a value in the config.
*
* @param option the option to check.
* @return true if option has been explicitly set.
* @see Option#isEnabled(OptionGroup)
* @see #hasValue(Option)
*/
public boolean isSet(Option<?> option) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
return config.isSet(getConfigKey(option));
}
/**
* Check if we have any value for this option (either set explicitly or some default value).
* Does not check if the value can be parsed.
*
* @param option the option to check.
* @return true if there is some value available for this option.
* @see Option#isEnabled(OptionGroup)
* @see #isSet(Option)
*/
public boolean hasValue(Option<?> option) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
String val = config.getValue(getConfigKey(option));
return val != null || option.getDefaultValue(this) != null;
}
/**
* Try to get the value of an option, or its default value.
* If no value and no default value is available, this returns null, even
* if the option is not optional.
* <p/>
* Note that after {@link #checkOptions()} has been called, this method
* should not throw any errors.
*
* @param option the option to query.
* @param <T> Type of the option
* @return The value, the default value, or null if no value is available.
* @throws IllegalArgumentException if the config-value cannot be parsed or is not valid.
*/
public <T> T tryGetOption(Option<T> option) throws IllegalArgumentException {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
String val = config.getValue(getConfigKey(option));
if (val == null) {
return option.getDefaultValue(this);
} else {
return option.parse(this, val);
}
}
/**
* Get the parsed default value from the config or from the option if not set.
*
* @param option the option to get the default value for.
* @param <T> the type of the value
* @return the default value, or null if no default is set in neither the config nor the option.
*/
public <T> T getDefaultValue(Option<T> option) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
String val = config.getDefaultValue(getConfigKey(option));
if (val == null) {
return option.getDefaultValue(this);
} else {
return option.parse(this, val);
}
}
/**
* Get the default value from the config or from the option if not set, but do not parse it or
* replace any keywords.
*
* @param option the option to get the default value for.
* @return the default value as set in the config file or the option.
*/
public String getDefaultValueText(Option option) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
String val = config.getDefaultValue(getConfigKey(option));
if (val == null) {
Object def = option.getDefaultValue();
return def != null ? def.toString() : null;
} else {
return val;
}
}
public <T> T getOption(Option<T> option, T defaultVal) throws IllegalArgumentException {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
T val = tryGetOption(option);
return val != null ? val : defaultVal;
}
/**
* Try to get the value of an option, or return null for optional options with no default value.
* An exception is thrown if no value (and no default value) is given and the option is not optional,
* or if the config-value cannot be parsed.
* <p/>
* Note that after {@link #checkOptions()} has been called, this method
* should not throw any errors.
*
* @param option the option to get the value for.
* @param <T> type of the option.
* @return the option value or null if
* @throws BadConfigurationError if the option is not defined in this group, if config-value cannot
* be parsed or if the option is null but required.
*/
public <T> T getOption(Option<T> option) throws BadConfigurationError {
if (!containsOption(option)) {
if(prefix != null) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
} else {
throw new BadConfigurationError("Global option "+option.getKey()+" is not known");
}
}
T opt;
try {
opt = tryGetOption(option);
} catch (IllegalArgumentException e) {
throw new Config.BadConfigurationError("Error parsing option '" + getConfigKey(option) + "': " + e.getMessage(), e);
}
if (opt == null && !option.isOptional()) {
throw new Config.BadConfigurationError("Missing required option: " + getConfigKey(option));
}
return opt;
}
public <T> void checkPresent(Option<T> option) throws Config.BadConfigurationException {
if (getOption(option) == null) {
throw new Config.BadConfigurationException("Missing option: " + getConfigKey(option));
}
}
public <T> void setOption(Option<T> option, T value) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
config.setProperty(getConfigKey(option), value.toString());
}
public <T> void setDefaultValue(Option<T> option, T value) {
if (!containsOption(option)) {
throw new BadConfigurationError("Option "+option.getKey()+" is not known in group "+prefix);
}
config.setDefaultProperty(getConfigKey(option), value.toString());
}
/*
* Parse, check and dump options
* ~~~~~~~~~~~~~~~~~~~~~~
*/
/**
* Consume all command line options and turn them into properties.<br/>
* <p/>
* <p>The arguments are processed as follows: If an argument is of the form
* "-option" or "--option", it is considered to be an option.
* If an argument is an option, the next argument is considered to be the parameter,
* unless the option is boolean and the next argument is missing or an option as well.
* We add the pair to our properties, consuming both arguments.
* </p><p>
* If an argument starts with @, the rest of it is considered as a property file name,
* which is then loaded and added to the configuration.
* The first non-option or the argument string {@code --} terminates the option list.
* </p>
*
* @param args The argument list
* @return An array of unconsumed arguments
* @throws Config.BadConfigurationException if an argument is malformed.
*/
public String[] consumeOptions(String[] args) throws Config.BadConfigurationException {
int i = 0;
while (i < args.length) {
if (commands.contains(args[i])) {
config.setProperty(getConfigKey(CMD_KEY), args[i]);
OptionGroup cmdGroup = subGroups.get(args[i]);
return cmdGroup.consumeOptions(Arrays.copyOfRange(args, i + 1, args.length));
}
// handle custom config files
if (args[i].startsWith("@")) {
String filename = args[i].substring(1);
try {
InputStream is = new BufferedInputStream(new FileInputStream(filename));
config.addProperties(is, prefix);
is.close();
} catch (FileNotFoundException e) {
throw new Config.BadConfigurationException("Configuration file '" + filename + "' not found!", e);
} catch (IOException e) {
throw new Config.BadConfigurationException("Error reading file '" + filename + "': " + e.getMessage(), e);
}
i++;
continue;
}
// break if this is not an option argument, return rest
if (!args[i].startsWith("-")) break;
if ("-".equals(args[i]) || "--".equals(args[i])) {
i++;
break;
}
String key = null;
if (args[i].charAt(1) == '-') key = args[i].substring(2);
else {
// for something of form '-<char>', try short option,
if (args[i].length() == 2) {
Option shortOption = getShortOptionKey(args[i].charAt(1));
if (shortOption != null) {
key = shortOption.getKey();
}
// for something of form '-<longtext>' try normal key for compatibility
} else {
key = args[i].substring(1);
}
}
i = parseOption(args, key, i);
i++;
}
return Arrays.copyOfRange(args, i, args.length);
}
protected int parseOption(String[] args, String key, int pos) throws BadConfigurationException {
Option<?> spec = getOptionSpec(key);
if (spec != null) {
if (isHidden(spec)) {
throw new BadConfigurationException("Invalid option: "+spec);
}
String val = null;
if (pos + 1 < args.length) {
String newVal = args[pos +1];
// allow to set to empty string
if ("''".equals(newVal) || "\"\"".equals(newVal)) {
newVal = "";
}
// TODO handle quoted arguments
if (spec.isValue(newVal)) {
val = newVal;
}
}
int i = pos;
if (spec instanceof BooleanOption && val == null) {
val = "true";
} else if (val == null) {
throw new BadConfigurationException("Missing argument for option: " + spec);
} else {
i++;
}
config.setProperty(getConfigKey(spec), val);
return i;
}
// maybe a boolean option, check for --no-<key>
if (key.startsWith("no-")) {
spec = getOptionSpec(key.substring(3));
if (isHidden(spec)) {
throw new BadConfigurationException("Invalid option: "+spec);
}
if (spec != null && spec instanceof BooleanOption) {
config.setProperty(getConfigKey(spec), "false");
} else if (spec != null) {
// unset it
config.setProperty(getConfigKey(spec), null);
}
// else spec == null;
} else if(key.contains(".")) {
// or maybe a sub-option
int j = key.indexOf('.');
OptionGroup group = subGroups.get(key.substring(0,j));
if (group != null) {
return group.parseOption(args, key.substring(j+1), pos);
}
}
if (spec == null) {
throw new BadConfigurationException("Unknown option: " + key);
}
return pos;
}
public void checkOptions() throws BadConfigurationException {
boolean skipCheck = false;
// first, check if we can parse all options and if we need to call the OptionChecker
for (Option<?> option : getOptions()) {
// check if we can parse
try {
tryGetOption(option);
} catch (IllegalArgumentException e) {
throw new BadConfigurationException("Error parsing option '" + getConfigKey(option) + "': " + e.getMessage(), e);
}
if (option.doSkipChecks() && option.isEnabled(this)) {
skipCheck = true;
}
}
// run the OptionChecker if required
if (!skipCheck) {
// check for required options
for (Option<?> option : availableOptions) {
if (!option.isOptional() && !hasValue(option) && isUsed(option)) {
throw new BadConfigurationException("Missing required option '" + getConfigKey(option) + '"');
}
}
// run OptionChecker
if (checker != null) checker.check(this);
}
}
/**
* Dump configuration of all options for debugging purposes
*
* @param p a writer to print the options to
* @param indent indent used for keys
* @return a set of all printed keys
*/
public Collection<String> printOptions(PrintStream p, int indent) {
Set<String> keys = new HashSet<String>();
for (Option<?> o : availableOptions()) {
String key = getConfigKey(o);
Object val = tryGetOption(o);
keys.add(key);
Config.printOption(p, indent, key, val);
}
for (OptionGroup group : subGroups.values()) {
keys.addAll(group.printOptions(p, indent));
}
return keys;
}
}