Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
* While not strictly 'validation', the spec also allows defining additional metadata like the 'default' keyword which
* is used in the merged result of a validation.
*
* Furthermore, a spec validation applies a limited set of type transformations.
*/
public class Spec {
private static final Logger log = LoggerFactory.getLogger(Spec.class);
/**
* Spec implementation that allows any key.
*/
public static final Spec ANY = new Spec();
static {
ANY.allowUnknownKeys = true;
}
private static final Spec OPTION_DESCRIPTOR = new Spec();
static {
OPTION_DESCRIPTOR.addOption("title", OptionType.STRING).withRequired(true);
OPTION_DESCRIPTOR.addOption("description", OptionType.LIST_OR_ELEMENT)
.withElementType(OptionType.STRING);
OPTION_DESCRIPTOR.addOption("type", OptionType.STRING)
.withRequired(true)
.withChoices(OptionType.class);
OPTION_DESCRIPTOR.addOption("required", OptionType.BOOLEAN).withDefault(false);
OPTION_DESCRIPTOR.addOption("secret", OptionType.BOOLEAN).withDefault(false);
OPTION_DESCRIPTOR.addOption("hidden", OptionType.BOOLEAN).withDefault(false);
OPTION_DESCRIPTOR.addOption("default", OptionType.ANY);
OPTION_DESCRIPTOR.addOption("versionAdded", OptionType.STRING);
OPTION_DESCRIPTOR.addOption("deprecationMessage", OptionType.STRING);
OPTION_DESCRIPTOR.addOption("elementType", OptionType.STRING)
.withChoices(OptionType.class);
OPTION_DESCRIPTOR.addOption("choices", OptionType.LIST)
.withElementType(OptionType.ANY);
OPTION_DESCRIPTOR.addOption("suboptions", OptionType.MAP)
.withSpec(ANY);
OPTION_DESCRIPTOR.addOption("applySpecDefaults", OptionType.BOOLEAN)
.withDefault(false);
}
private Map options = new LinkedHashMap<>();
private Map aliases = new HashMap<>();
private boolean allowUnknownKeys = false;
private List> requiredOneOfGroups = new ArrayList<>(0);
private List> requireTogetherGroups = new ArrayList<>(0);
private List> mutuallyExclusiveGroups = new ArrayList<>(0);
private List whenConditions = new ArrayList<>(0);
/**
* Returns true if this spec contains the specified option.
*/
public boolean containsOption(String name) {
return options.containsKey(name);
}
/**
* Add an {@link Option} to this spec.
*
* @throws IllegalArgumentException
* if an option with this name is already defined.
*/
public Option addOption(String name, OptionType type) {
if (options.containsKey(name) || aliases.containsKey(name)) {
throw new IllegalArgumentException("Option '" + name + "' is already defined");
}
var option = new Option(this, name, type);
options.put(name, option);
return option;
}
/**
* Remove an {@link Option} from this spec.
*/
public void removeOption(String name) {
options.remove(name);
}
public void allowUnknownKeys(boolean allowUnknownKeys) {
this.allowUnknownKeys = allowUnknownKeys;
}
/**
* Specify a set of keys of which at least one must be specified. Note that this not enforce that only one is
* specified. You can combine this check with {@link #mutuallyExclusive(String...)} if that is required.
*/
public void requireOneOf(String... keys) {
verifyKeys(keys);
requiredOneOfGroups.add(Arrays.asList(keys));
}
/**
* Specify a set of keys that must appear together. This check only applies as soon as at least one of these keys
* has been specified.
*/
public void requireTogether(String... keys) {
verifyKeys(keys);
requireTogetherGroups.add(Arrays.asList(keys));
}
/**
* Specify a set of keys that are mutually exclusive. i.e. at most one of them may be specified.
*/
public void mutuallyExclusive(String... keys) {
verifyKeys(keys);
mutuallyExclusiveGroups.add(Arrays.asList(keys));
}
/**
* Add a condition that is only verified when {@code key.equals(value)}
*
* @param key
* the name of an option
* @param value
* the value that triggers the conditional check
* @return an instance of {@link WhenCondition} for further configuration options
*/
public WhenCondition when(String key, Object value) {
verifyKeys(key);
var whenCondition = new WhenCondition(this, key, value);
whenConditions.add(whenCondition);
return whenCondition;
}
/**
* Validate the given arguments according to this spec.
*
* @param args
* the arguments to validate.
* @return the validation result where defaults have been added to the input arguments
* @throws ValidationException
* when the specified arguments did not match this specification
*/
public YConfiguration validate(YConfiguration args) throws ValidationException {
var ctx = new ValidationContext(args.getPath());
var result = doValidate(ctx, args.getRoot(), "", false);
var wrapped = YConfiguration.wrap(result);
wrapped.parent = args.parent;
wrapped.parentKey = args.parentKey;
wrapped.rootLocation = args.rootLocation;
return wrapped;
}
/**
* Validate the given arguments according to this spec.
*
* @param args
* the arguments to validate, keyed by argument name.
* @return the validation result where defaults have been added to the input arguments
* @throws ValidationException
* when the specified arguments did not match this specification
*/
public Map validate(Map args) throws ValidationException {
return doValidate(new ValidationContext(""), args, "", false);
}
private Map doValidate(ValidationContext ctx, Map args, String parent,
boolean suppressWarnings)
throws ValidationException {
for (var group : requiredOneOfGroups) {
if (count(args, group) == 0) {
var msg = "One of the following is required: " + group;
if (!"".equals(parent)) {
msg += " at " + parent;
}
throw new ValidationException(ctx, msg);
}
}
for (var group : mutuallyExclusiveGroups) {
if (count(args, group) > 1) {
var msg = "The following arguments are mutually exclusive: " + group;
if (!"".equals(parent)) {
msg += " at " + parent;
}
throw new ValidationException(ctx, msg);
}
}
for (var group : requireTogetherGroups) {
int n = count(args, group);
if (n > 0 && n != group.size()) {
var msg = "The following arguments are required together: " + group;
if (!"".equals(parent)) {
msg += " at " + parent;
}
throw new ValidationException(ctx, msg);
}
}
for (var whenCondition : whenConditions) {
var arg = args.get(whenCondition.key);
if (arg != null && arg.equals(whenCondition.value)) {
var missing = whenCondition.requiredKeys.stream()
.filter(key -> !args.containsKey(key))
.collect(Collectors.toList());
if (!missing.isEmpty()) {
var path = "".equals(parent) ? whenCondition.key : (parent + "->" + whenCondition.key);
throw new ValidationException(ctx, String.format(
"%s is %s but the following arguments are missing: %s",
path, whenCondition.value, missing));
}
}
}
// Build a new set of args where defaults have been entered
// Makes this a linked hashmap to keep the defined order
var result = new LinkedHashMap();
// Check the provided arguments
for (var entry : args.entrySet()) {
var argName = entry.getKey();
var path = "".equals(parent) ? argName : (parent + "->" + argName);
var option = getOption(argName);
if (option == null) {
if (allowUnknownKeys) {
result.put(argName, entry.getValue());
} else {
throw new ValidationException(ctx, "Unknown argument " + path);
}
} else if (result.containsKey(option.name)) {
throw new ValidationException(ctx,
String.format("Argument '%s' already specified. Check for aliases.", option.name));
} else {
var arg = entry.getValue();
var resultArg = option.validate(ctx, arg, path, suppressWarnings);
result.put(option.name, resultArg);
}
}
for (var option : options.values()) {
var specified = args.containsKey(option.name);
for (var alias : aliases.entrySet()) {
if (alias.getValue().equals(option.name) && args.containsKey(alias.getKey())) {
specified = true;
}
}
if (!specified) {
var path = "".equals(parent) ? option.name : parent + "->" + option.name;
if (option.required) {
throw new ValidationException(ctx, "Missing required argument " + path);
}
var defaultValue = option.validate(ctx, option.computeDefaultValue(), path,
true /* suppressWarnings */);
if (defaultValue != null) {
result.put(option.name, defaultValue);
}
}
}
return result;
}
public Collection