package org.ovirt.engine.core.utils.vmproperties;
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.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
import org.ovirt.engine.core.common.businessentities.VmStatic;
import org.ovirt.engine.core.common.config.Config;
import org.ovirt.engine.core.common.config.ConfigValues;
import org.ovirt.engine.core.compat.StringHelper;
/**
* Helper methods to help parse and validate predefined and UserDefined(user defined) properties. These methods are used
* by vdsbroker and bll modules
*
*/
public class VmPropertiesUtils {
private static final Pattern SEMICOLON_PATTERN = Pattern.compile(";");
private static final String PROPERTIES_DELIMETER = ";";
private static final String KEY_VALUE_DELIMETER = "=";
private static final String LEGITIMATE_CHARACTER_FOR_KEY = "[a-z_A-Z0-9]";
private static final String LEGITIMATE_CHARACTER_FOR_VALUE = "[^" + PROPERTIES_DELIMETER + "]"; // all characters
// but the delimiter
// are allowed
private static final Pattern KEY_PATTERN = Pattern.compile("(" + LEGITIMATE_CHARACTER_FOR_KEY + ")+");
private static final Pattern VALUE_PATTERN = Pattern.compile("(" + LEGITIMATE_CHARACTER_FOR_VALUE + ")+");
// private static final Pattern KEY_VALIDATION_PATTERN = Pattern.compile("(" + LEGITIMATE_CHARACTER + ")+");
// properties are in form of key1=val1;key2=val2; .... key and include alpha numeric characters and _
private static final String KEY_VALUE_REGEX_STR = "((" + LEGITIMATE_CHARACTER_FOR_KEY + ")+)=(("
+ LEGITIMATE_CHARACTER_FOR_VALUE + ")+)";
// frontend can pass custom values in the form of "key=value" or "key1=value1;... key-n=value_n" (if there is only
// one key-value, no ; is attached to it
private static final Pattern VALIDATION_PATTERN = Pattern.compile(KEY_VALUE_REGEX_STR + "(;" + KEY_VALUE_REGEX_STR
+ ")*;?");
private static final List<ValidationError> invalidSyntaxValidationError =
Arrays.asList(new ValidationError(ValidationFailureReason.SYNTAX_ERROR, ""));
private static Map<String, Pattern> predefinedProperties = new HashMap<String, Pattern>();
private static Map<String, Pattern> userdefinedProperties = new HashMap<String, Pattern>();
// Defines why validation failed
public static enum ValidationFailureReason {
KEY_DOES_NOT_EXIST,
INCORRECT_VALUE,
SYNTAX_ERROR,
DUPLICATE_KEY,
NO_ERROR
};
public static class ValidationError {
private ValidationFailureReason reason;
private String keyName;
public ValidationError(ValidationFailureReason reason, String keyName) {
this.reason = reason;
this.keyName = keyName;
}
public ValidationFailureReason getReason() {
return reason;
}
public String getKeyName() {
return keyName;
}
@Override
public boolean equals(Object other) {
if (other != null && other instanceof ValidationError) {
ValidationError otherError = (ValidationError) other;
return keyName.equals(otherError.getKeyName()) && reason == otherError.getReason();
}
return false;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result
+ ((keyName == null) ? 0 : keyName.hashCode());
result = prime * result + ((reason == null) ? 0 : reason.hashCode());
return result;
}
public boolean isOK() {
return reason == ValidationFailureReason.NO_ERROR;
}
}
static {
String predefinedVMPropertiesStr = Config.<String> GetValue(ConfigValues.PredefinedVMProperties, "3.0");
parseVMPropertiesRegex(predefinedVMPropertiesStr, predefinedProperties);
String userDefinedVMPropertiesStr = Config.<String> GetValue(ConfigValues.UserDefinedVMProperties, "3.0");
parseVMPropertiesRegex(userDefinedVMPropertiesStr, userdefinedProperties);
}
public static class VMCustomProperties {
private String predefinedProperties;
private String userDefinedProperties;
public VMCustomProperties(String predefinedProperties, String userDefinedProperties) {
this.predefinedProperties = predefinedProperties;
this.userDefinedProperties = userDefinedProperties;
}
public String getPredefinedProperties() {
return predefinedProperties;
}
public String getUseDefinedProperties() {
return userDefinedProperties;
}
}
/**
* Parses a string containing user defined and predefined custom properties and returns VMCustomProperties object
* that contains the properties separated to two strings - one for the predefined properties and one for the user
* defined properties
*
* @param propertiesStr
* @return
*/
public static VMCustomProperties parseProperties(String propertiesStr) {
HashMap<String, String> userDefinedPropertiesMap = new HashMap<String, String>();
HashMap<String, String> predefinedPropertiesMap = new HashMap<String, String>();
convertCustomPropertiesStrToMaps(propertiesStr, predefinedPropertiesMap, userDefinedPropertiesMap);
return new VMCustomProperties(vmPropertiesToString(predefinedPropertiesMap),
vmPropertiesToString(userDefinedPropertiesMap));
}
/**
* Validates all the custom properties of a VM
*
* @param vmStatic
* @return
*/
public static List<ValidationError> validateVMProperties(VmStatic vmStatic) {
List<ValidationError> errors = validateProperties(vmStatic.getCustomProperties());
return errors;
}
private static void parseVMPropertiesRegex(String properties, Map<String, Pattern> keysToRegex) {
if (StringHelper.isNullOrEmpty(properties)) {
return;
}
String[] propertiesStrs = SEMICOLON_PATTERN.split(properties);
// Property is in the form of key=regex
for (String property : propertiesStrs) {
Pattern pattern = null;
String[] propertyParts = property.split(KEY_VALUE_DELIMETER);
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 delimeter
pattern = VALUE_PATTERN;
} else {
pattern = Pattern.compile(propertyParts[1]);
}
keysToRegex.put(propertyParts[0], pattern);
}
}
/**
* Validates a properties field value (checks if its format matches key1=val1;key2=val2;....)
*
* @param fieldValue
* @return a list of validation errors. if there are no errors - the list will be empty
*/
private static List<ValidationError> validateProperties(String properties) {
if (StringHelper.isNullOrEmpty(properties)) { // No errors in case of empty value
return Collections.emptyList();
}
if (syntaxErrorInProperties(properties)) {
return invalidSyntaxValidationError;
}
// Transform the VM custom properties from string value to hash map
// check the following for each one of the keys:
// 1. Check if the key exists in either the predefined or the userdefined key sets
// 2. Check if the value of the key is valid
// In case either 1 or 2 fails, add an error to the errors list
Map<String, String> map = new HashMap<String, String>();
List<ValidationError> result = populateVMProperties(properties, map);
return result;
}
private static boolean syntaxErrorInProperties(String properties) {
return !VALIDATION_PATTERN.matcher(properties).matches();
}
private static boolean isValueValid(String key, String value) {
// Checks that the value for the given property is valid by running by trying to perform
// regex validation
Pattern userDefinedPattern = userdefinedProperties.get(key);
Pattern predefinedPattern = predefinedProperties.get(key);
return (userDefinedPattern != null && userDefinedPattern.matcher(value).matches())
|| (predefinedPattern != null && predefinedPattern.matcher(value).matches());
}
/**
* Get a map containing all the VM custom properties
*
* @return map containing the VM custom properties
*/
public static Map<String, String> getVMProperties(VmStatic vmStatic) {
separeteCustomPropertiesToUserAndPredefined(vmStatic);
Map<String, String> map = new HashMap<String, String>();
getPredefinedProperties(vmStatic, map);
getUserDefinedProperties(vmStatic, map);
return map;
}
/**
* Get a map containing user defined properties from VM
*
* @param vm
* vm to get the map for
* @return map containing the UserDefined properties
*/
public static Map<String, String> getUserDefinedProperties(VmStatic vmStatic) {
Map<String, String> map = new HashMap<String, String>();
getUserDefinedProperties(vmStatic, map);
return map;
}
/**
* Gets a map containing the predefined properties from VM
*
* @param vm
* vm to get the map for
* @return map containing the vm properties
*/
public static Map<String, String> getPredefinedProperties(VmStatic vmStatic) {
Map<String, String> map = new HashMap<String, String>();
getPredefinedProperties(vmStatic, map);
return map;
}
private static void getPredefinedProperties(VmStatic vmStatic, Map<String, String> propertiesMap) {
String predefinedProperties = vmStatic.getPredefinedProperties();
getVMProperties(propertiesMap, predefinedProperties);
}
private static void getUserDefinedProperties(VmStatic vmStatic, Map<String, String> propertiesMap) {
String UserDefinedProperties = vmStatic.getUserDefinedProperties();
getVMProperties(propertiesMap, UserDefinedProperties);
}
/**
* Converts VM properties field value (either UserDefined or defined) to a map containing property names & values
*
* @param propertiesMap
* map that will hold the conversion result
* @param vmPropertiesFieldValue
* the string value that contains the properties
*/
public static void getVMProperties(Map<String, String> propertiesMap, String vmPropertiesFieldValue) {
// format of properties is key1=val1,key2=val2,key3=val3,key4=val4
if (StringHelper.isNullOrEmpty(vmPropertiesFieldValue)) {
return;
}
populateVMProperties(vmPropertiesFieldValue, propertiesMap);
}
/**
* Parses a string of VM properties to a map of key,value
*
* @param vmPropertiesFieldValue
* the string to parse
* @param propertiesMap
* the filled map
* @return list of errors during parsing (currently holds list of duplicate keys)
*/
private static List<ValidationError> populateVMProperties(String vmPropertiesFieldValue,
Map<String, String> propertiesMap) {
Set<ValidationError> errorsSet = new HashSet<VmPropertiesUtils.ValidationError>();
List<ValidationError> results = new ArrayList<VmPropertiesUtils.ValidationError>();
if (!StringHelper.isNullOrEmpty(vmPropertiesFieldValue)) {
String keyValuePairs[] = SEMICOLON_PATTERN.split(vmPropertiesFieldValue);
for (String keyValuePairStr : keyValuePairs) {
String[] pairParts = keyValuePairStr.split(KEY_VALUE_DELIMETER, 2);
String key = pairParts[0];
String value = pairParts[1];
if (propertiesMap.containsKey(key)) {
errorsSet.add(new ValidationError(ValidationFailureReason.DUPLICATE_KEY, key));
continue;
}
if (!predefinedProperties.containsKey(key) && !userdefinedProperties.containsKey(key)) {
errorsSet.add(new ValidationError(ValidationFailureReason.KEY_DOES_NOT_EXIST, key));
continue;
}
if (!isValueValid(key, value)) {
errorsSet.add(new ValidationError(ValidationFailureReason.INCORRECT_VALUE, key));
continue;
}
propertiesMap.put(key, value);
}
}
results.addAll(errorsSet);
return results;
}
private static String vmPropertiesToString(Map<String, String> propertiesMap) {
if (propertiesMap == null || propertiesMap.size() == 0) {
return "";
}
StringBuilder result = new StringBuilder();
Set<Entry<String, String>> entries = propertiesMap.entrySet();
Iterator<Entry<String, String>> iterator = entries.iterator();
Entry<String, String> entry = iterator.next();
result.append(entry.getKey()).append("=").append(entry.getValue());
while (iterator.hasNext()) {
result.append(";");
entry = iterator.next();
if (entry != null) {
result.append(entry.getKey()).append("=").append(entry.getValue());
}
}
return result.toString();
}
private static void convertCustomPropertiesStrToMaps(String propertiesValue,
Map<String, String> predefinedPropertiesMap, Map<String, String> userDefinedPropertiesMap) {
Map<String, String> propertiesMap = new HashMap<String, String>();
populateVMProperties(propertiesValue, propertiesMap);
Set<Entry<String, String>> propertiesEntries = propertiesMap.entrySet();
// Go over all the properties - if the key of the property exists in the
// predefined key set -
// add it to the predefined map, otherwise - add it to the user defined
// map
Set<String> predefinedPropertiesKeys = predefinedProperties.keySet();
Set<String> userdefinedPropertiesKeys = userdefinedProperties.keySet();
for (Entry<String, String> propertiesEntry : propertiesEntries) {
String propertyKey = propertiesEntry.getKey();
String propertyValue = propertiesEntry.getValue();
if (predefinedPropertiesKeys.contains(propertyKey)) {
predefinedPropertiesMap.put(propertyKey, propertyValue);
}
if (userdefinedPropertiesKeys.contains(propertyKey)) {
userDefinedPropertiesMap.put(propertyKey, propertyValue);
}
}
}
/**
* Composes custom properties string from predefined properties and user defined properties strings
*
* @param predefinedProperties
* @param userDefinedProperties
* @return
*/
public static String customProperties(String predefinedProperties, String userDefinedProperties) {
StringBuilder result = new StringBuilder();
result.append((StringHelper.isNullOrEmpty(predefinedProperties)) ? "" : predefinedProperties);
result.append((result.length() == 0) ? "" : ";");
result.append((StringHelper.isNullOrEmpty(userDefinedProperties)) ? "" : userDefinedProperties);
return result.toString();
}
/**
* Splits the validation errors list to lists of missing keys and wrong key values
*
* @param errorsList
* @param missingKeysList
* @param wrongKeyValues
*/
public static 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<VmPropertiesUtils.ValidationError>();
resultMap.put(error.getReason(), errorsForReason);
}
errorsForReason.add(error);
}
}
/**
* Generates an error message to be displayed by frontend
*
* @param errorsList
* list of errors obtained from parsing
* @param syntaxErrorMessage
* error to be displayed in case of syntax error
* @param failureReasonsToVdcBllMessagesMap
* map of errors to vdc bll messages (for presentation of errors from AppErrors.resx)
* @param failureReasonsToParameterFormatStrings
* map of errors to format string (for presentation of values for the errors are defined in
* AppError.resx)
* @return
*/
public static List<String> generateErrorMessages(List<ValidationError> errorsList,
String syntaxErrorMessage, Map<ValidationFailureReason, String> failureReasonsToVdcBllMessagesMap,
Map<ValidationFailureReason, String> failureReasonsToParameterFormatStrings) {
// No errors , the returned list reported to client will be empty
if (errorsList == null || errorsList.isEmpty()) {
return Collections.emptyList();
}
// Syntax error is the most severe error, and should be returned without checking the rest of the errors
if (errorsList.size() == 1 && errorsList.get(0).getReason() == ValidationFailureReason.SYNTAX_ERROR) {
return Arrays.asList(syntaxErrorMessage);
}
// Check all the errors and for each error add it ands its arguments to the returned list
List<String> result = new ArrayList<String>();
Map<ValidationFailureReason, List<ValidationError>> resultMap =
new HashMap<VmPropertiesUtils.ValidationFailureReason, List<ValidationError>>();
separateValidationErrorsList(errorsList, resultMap);
for (Map.Entry<ValidationFailureReason, String> entry : failureReasonsToVdcBllMessagesMap.entrySet()) {
List<ValidationError> errorsListForReason = resultMap.get(entry.getKey());
String vdcBllMessage = entry.getValue();
String formatString = failureReasonsToParameterFormatStrings.get(entry.getKey());
addMessage(result, vdcBllMessage, formatString, errorsListForReason);
}
return result;
}
private static void addMessage(List<String> resultMessages,
String vdcBllMessage,
String formatString, List<ValidationError> errorsList) {
if (errorsList != null && !errorsList.isEmpty()) {
String keys = getCommaDelimitedKeys(errorsList);
resultMessages.add(vdcBllMessage);
resultMessages.add(String.format(formatString, keys));
}
}
private static 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();
}
public static void separeteCustomPropertiesToUserAndPredefined(VmStatic vmStatic) {
String customProperties = vmStatic.getCustomProperties();
VMCustomProperties properties = VmPropertiesUtils.parseProperties(customProperties);
vmStatic.setPredefinedProperties(properties.getPredefinedProperties());
vmStatic.setUserDefinedProperties(properties.getUseDefinedProperties());
}
}