package org.ovirt.engine.core.common.utils.customprop;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.ovirt.engine.core.common.config.Config;
import org.ovirt.engine.core.common.config.ConfigValues;
import org.ovirt.engine.core.compat.StringHelper;
import org.ovirt.engine.core.compat.Version;
/**
* Abstract class to ease custom properties handling
*
*/
public class CustomPropertiesUtils {
/**
* Delimiter of each property definition
*/
protected static final String PROPERTIES_DELIMETER = ";";
/**
* Delimiter of property name and value
*/
protected static final String KEY_VALUE_DELIMETER = "=";
/**
* Regex describing legitimate characters for property name - alphanumeric characters and underscore
*/
protected static final String LEGITIMATE_CHARACTER_FOR_KEY = "[a-z_A-Z0-9]";
/**
* Regex describing property name
*/
protected static final String KEY_REGEX = "(" + LEGITIMATE_CHARACTER_FOR_KEY + ")+";
/**
* Regex describing legitimate characters for property value - all except {@code PROPERTIES_DELIMITER}
*/
protected static final String LEGITIMATE_CHARACTER_FOR_VALUE = "[^" + PROPERTIES_DELIMETER + "]";
/**
* Regex describing property value
*/
protected static final String VALUE_REGEX = "(" + LEGITIMATE_CHARACTER_FOR_VALUE + ")*";
/**
* Regex describing property definition - key=value
*/
protected static final String KEY_VALUE_REGEX_STR = "((" + LEGITIMATE_CHARACTER_FOR_KEY + ")+)=(("
+ LEGITIMATE_CHARACTER_FOR_VALUE + ")*)";
/**
* Regex describing properties definition. They can be in the from of "key=value" or "key1=value1;... key-n=value_n"
* (last {@code ;} character can be omitted)
*/
protected static final String VALIDATION_STR = "(" + KEY_VALUE_REGEX_STR + "(;" + KEY_VALUE_REGEX_STR + ")*;?)?";
/**
* List defining syntax error during properties validation
*/
protected final List<ValidationError> invalidSyntaxValidationError;
/**
* Constructor has package access to enable testing, but class cannot be instantiated outside package
*/
protected CustomPropertiesUtils() {
invalidSyntaxValidationError = Arrays.asList(new ValidationError(ValidationFailureReason.SYNTAX_ERROR, ""));
}
/**
* Returns supported cluster levels. Method should be used only for testing.
*
* @return supported cluster levels
*/
public Set<Version> getSupportedClusterLevels() {
Set<Version> versions = Config.<HashSet<Version>> getValue(ConfigValues.SupportedClusterLevels);
return versions;
}
/**
* Test if custom properties contains syntax error
*
* @param properties
* custom properties
* @return returns {@code true} if custom properties contains syntax error, otherwise {@code false}
*/
public boolean syntaxErrorInProperties(String properties) {
return properties != null && !properties.matches(VALIDATION_STR);
}
/**
* Test if custom properties contains syntax error
*
* @param properties
* custom properties
* @return returns {@code true} if custom properties contains syntax error, otherwise {@code false}
*/
public boolean syntaxErrorInProperties(Map<String, String> properties) {
boolean error = false;
if (properties != null && !properties.isEmpty()) {
for (Map.Entry<String, String> e : properties.entrySet()) {
String key = e.getKey();
if (key == null || !key.matches(KEY_REGEX)) {
// syntax error in property name
error = true;
break;
}
if (!StringHelper.defaultString(e.getValue()).matches(VALUE_REGEX)) {
// syntax error in property value
error = true;
break;
}
}
}
return error;
}
/**
* Converts properties specification from {@code String} to {@code Map<String, Pattern}
*/
protected void parsePropertiesRegex(String properties, Map<String, String> keysToRegex) {
if (StringHelper.isNullOrEmpty(properties)) {
return;
}
String[] propertiesStrs = properties.split(PROPERTIES_DELIMETER);
// Property is in the form of key=regex
for (String property : propertiesStrs) {
String pattern = null;
String[] propertyParts = property.split(KEY_VALUE_DELIMETER, 2);
if (propertyParts.length == 1) {
// there is no value(regex) for the property - we assume in that case that any value is allowed except
// for the properties delimiter
pattern = VALUE_REGEX;
} else {
pattern = propertyParts[1];
}
keysToRegex.put(propertyParts[0], pattern);
}
}
/**
* Splits the validation errors list to lists of missing keys and wrong key values
*/
protected void separateValidationErrorsList(List<ValidationError> errorsList,
Map<ValidationFailureReason, List<ValidationError>> resultMap) {
if (errorsList == null || errorsList.isEmpty()) {
return;
}
for (ValidationError error : errorsList) {
List<ValidationError> errorsForReason = resultMap.get(error.getReason());
if (errorsForReason == null) {
errorsForReason = new ArrayList<>();
resultMap.put(error.getReason(), errorsForReason);
}
errorsForReason.add(error);
}
}
/**
* validate a map of specific custom properties against provided regex map
* @param regExMap
* [key, regex] map
* @param properties
* [key, value] map, custom properties to validate
*/
public List<ValidationError> validateProperties(Map<String, String> regExMap,
Map<String, String> properties) {
if (properties == null || properties.isEmpty()) {
// No errors in case of empty value
return Collections.emptyList();
}
if (syntaxErrorInProperties(properties)) {
return invalidSyntaxValidationError;
}
Set<ValidationError> errorsSet = new HashSet<>();
Set<String> foundKeys = new HashSet<>();
for (Entry<String, String> e : properties.entrySet()) {
String key = e.getKey();
if (foundKeys.contains(key)) {
errorsSet.add(new ValidationError(ValidationFailureReason.DUPLICATE_KEY, key));
continue;
}
foundKeys.add(key);
if (key == null || !regExMap.containsKey(key)) {
errorsSet.add(new ValidationError(ValidationFailureReason.KEY_DOES_NOT_EXIST, key));
continue;
}
String value = e.getValue() == null ? "" : e.getValue();
if (!value.matches(regExMap.get(key))) {
errorsSet.add(new ValidationError(ValidationFailureReason.INCORRECT_VALUE, key));
continue;
}
}
List<ValidationError> results = new ArrayList<>();
results.addAll(errorsSet);
return results;
}
/**
* Generates an error message to be displayed by frontend
*
* @param validationErrors
* list of errors appeared during validation
* @param message
* list of error messages to display
*/
public void handleCustomPropertiesError(List<ValidationError> validationErrors, List<String> message) {
// No errors
if (validationErrors == null || validationErrors.isEmpty()) {
return;
}
// Syntax error is one of the most severe errors, and should be returned without checking the rest of the errors
if (validationErrors.size() == 1 && validationErrors.get(0).getReason() == ValidationFailureReason.SYNTAX_ERROR) {
message.add(ValidationFailureReason.SYNTAX_ERROR.getErrorMessage().name());
return;
}
// Check all the errors and for each error add it ands its arguments to the returned list
Map<ValidationFailureReason, List<ValidationError>> resultMap =
new HashMap<>();
separateValidationErrorsList(validationErrors, resultMap);
for (ValidationFailureReason reason : ValidationFailureReason.values()) {
List<ValidationError> errorsListForReason = resultMap.get(reason);
if (errorsListForReason != null && !errorsListForReason.isEmpty()) {
String keys = getCommaDelimitedKeys(errorsListForReason);
message.add(reason.getErrorMessage().name());
message.add(reason.formatErrorMessage(keys));
}
}
}
/**
* Returns string containing comma separated list of all property names appeared in error list
*
* @param validationErrors
* error list
* @return string containing comma separated list of all property names appeared in error list
*/
protected String getCommaDelimitedKeys(List<ValidationError> validationErrors) {
if (validationErrors == null || validationErrors.isEmpty()) {
return "";
}
StringBuilder sb = new StringBuilder();
Iterator<ValidationError> iterator = validationErrors.iterator();
for (int counter = 0; counter < validationErrors.size() - 1; counter++) {
ValidationError error = iterator.next();
sb.append(error.getKeyName()).append(",");
}
ValidationError error = iterator.next();
sb.append(error.getKeyName());
return sb.toString();
}
protected Map<String, String> convertProperties(String properties, Map<String, String> regExMap) {
Map<String, String> map = new LinkedHashMap<>();
if (!StringHelper.isNullOrEmpty(properties)) {
String[] keyValuePairs = properties.split(PROPERTIES_DELIMETER);
for (String keyValuePairStr : keyValuePairs) {
String[] pairParts = keyValuePairStr.split(KEY_VALUE_DELIMETER, 2);
String key = pairParts[0];
String value = pairParts[1];
map.put(key, value);
}
}
if (regExMap != null) {
for (ValidationError error : validateProperties(regExMap, map)) {
map.remove(error.getKeyName());
}
}
return map;
}
/**
* Converts device custom properties from string to map.
*
* @param properties
* specified device properties
* @return map containing all device custom properties ({@code LinkedHashMap} is used to ensure properties order is
* constant)
* @exception IllegalArgumentException
* if specified properties has syntax errors
*/
public Map<String, String> convertProperties(String properties) {
return convertProperties(properties, null);
}
/**
* Converts device custom properties from map to string.
*
* @param properties
* specified device properties
* @return string containing all properties in map
* @exception IllegalArgumentException
* if specified properties has syntax errors
*/
public String convertProperties(Map<String, String> properties) {
StringBuilder sb = new StringBuilder();
if (properties != null && !properties.isEmpty()) {
for (Map.Entry<String, String> e : properties.entrySet()) {
sb.append(e.getKey());
sb.append(KEY_VALUE_DELIMETER);
sb.append(StringHelper.defaultString(e.getValue()));
sb.append(PROPERTIES_DELIMETER);
}
// remove last PROPERTIES_DELIMETER
sb.deleteCharAt(sb.length() - 1);
}
return sb.toString();
}
}