/* * Copyright (c) 2014 EMC Corporation * All Rights Reserved */ package com.emc.storageos.customconfigcontroller; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.emc.storageos.customconfigcontroller.exceptions.CustomConfigControllerException; import com.emc.storageos.db.client.model.StringMap; public class CustomNameResolver extends CustomConfigResolver { private static final long serialVersionUID = 7846294202585888699L; private static final Logger logger = LoggerFactory .getLogger(CustomNameResolver.class); private static final String START_DELIMITER = "{"; private static final String END_DELIMITER = "}"; private static final String METHOD_NAMES = "%METHODS%"; private static final String START_PAREN = "("; private static final String END_PAREN = ")"; // Regex to find the first open parenthesis that is not proceeded by one of the method names // The code will loop to find the inner-most expression, the regex only needs to find the first private String NESTED_EXPRESSION_START = "(?<!" + METHOD_NAMES + ")+\\((.*?)$"; // Regex to find all chained methods that apply to an expression private static final String METHODS_EXPRESSION = "(\\.(" + METHOD_NAMES + ")\\([^\\)]*\\))+"; // Regex to find . not proceeded by a \ i.e. not escaped public static final String DOT_EXPRESSION = "(?<!" + Pattern.quote("\\") + ")" + Pattern.quote("."); // The maximum number of time the name resolution code is allowed to loop trying // to resolve an expression. This is a protection measure against infinite loops. // If we loop more than MAX_LOOP, the expression is most probably not resolvable public static final int MAX_LOOP = 100; private List<CustomConfigMethod> stringManipulationMethods; public List<CustomConfigMethod> getStringManipulationMethods() { return stringManipulationMethods; } public void setStringManipulationMethods( List<CustomConfigMethod> stringManipulationMethods) { this.stringManipulationMethods = stringManipulationMethods; } public CustomConfigMethod getCustomConfigMethod(String methodName) { for (CustomConfigMethod method : stringManipulationMethods) { if (method.getName().equals(methodName)) { return method; } } return null; } /** * Gets an array of all supported method display names * * @return an array of all supported method names */ public String[] getCustomConfigMethodNames() { String[] names = new String[stringManipulationMethods.size()]; for (int i = 0; i < stringManipulationMethods.size(); i++) { names[i] = stringManipulationMethods.get(i).getName(); } return names; } /** * The expression is expected to be in the format * datasource.operation(params..).operation(params...) * * @param expression * @param datasource * @return */ private String getDataSourceValue(String expression, DataSource datasource, IllegalCharsConstraint constraint, String systemType) { // Split the expression based on dot String[] tokenArray = expression.split(DOT_EXPRESSION); // first token is the field String datasourceField = tokenArray[0]; String fieldValue = (String) datasource .getPropertyValue(datasourceField); if (fieldValue == null) { logger.error("Couldn't find a datasource property with name {}", datasourceField); fieldValue = ""; } else { fieldValue = processStringMethods(tokenArray, fieldValue); } return fieldValue; } /** * Given an expression, perform the list of string manipulation functions of * it. * * @param array * of string expressions of methods in the format * operation(param...). Note, the first method is expected to be * at index 1 in the array. The methid has to be one that is * defined in {@link #getStringManipulationMethods()} * @param value * the string to be manipulated * @return the resolved string */ private String processStringMethods(String[] tokens, String value) { String fieldValue = value; for (int i = 1; i < tokens.length; i++) { String[] operationTokens = StringUtils.split(tokens[i].replaceAll("\\s+", ""), ",()"); // first token is the method name, remaining tokens are the method // arguments List<String> methodArgs = new ArrayList<String>(); String stringMethod = operationTokens[0]; for (int argIndex = 1; argIndex < operationTokens.length; argIndex++) { if (operationTokens[argIndex].startsWith("\"") && operationTokens[argIndex].endsWith("\"")) { // it is a string - strip out the quotes methodArgs.add(operationTokens[argIndex].substring(1, operationTokens[argIndex].length() - 1)); } else { methodArgs.add(operationTokens[argIndex]); } } fieldValue = invokeStringMethod(fieldValue, stringMethod, methodArgs); } return fieldValue; } /** * Given a method name and its arguments, invoke the method and return its * results. * * @param stringObj * the string to be transformed * @param methodName * the name of the method which has to be one of the methods in {@link #getStringManipulationMethods()} * @param methodArgs * the method arguments * @return */ public String invokeStringMethod(String stringObj, String methodName, List<String> methodArgs) { String value = ""; try { CustomConfigMethod configMethodDef = getCustomConfigMethod(methodName); if (configMethodDef == null) { logger.error( "Couldn't find a string manipulation function with name {}", methodName); return null; } logger.debug("Invoking string method on string: {}", stringObj); logger.debug("Method: {} with method args: {}", configMethodDef.getName(), methodArgs); value = configMethodDef.invoke(stringObj, methodArgs); } catch (Exception e) { logger.error("Exception while invoking string method-", e); } return value; } @Override public void validate(CustomConfigType configTemplate, StringMap scope, String value) { simpleSyntaxValidation(value); // Parse the value and validate the datasource properties StringTokenizer tokenizer = new StringTokenizer(value, START_DELIMITER + END_DELIMITER, true); // make a map of datasource property and their names. Use this for // datasource property validation Map<DataSourceVariable, String> datasourcePropMap = new HashMap<DataSourceVariable, String>(); for (DataSourceVariable datasourceProp : configTemplate .getDataSourceVariables().keySet()) { datasourcePropMap.put(datasourceProp, datasourceProp.getDisplayName()); } while (tokenizer.hasMoreTokens()) { if (tokenizer.nextToken() .equals(CustomNameResolver.START_DELIMITER) && tokenizer.hasMoreTokens()) { // this is a datasource property which should be computed String expression = tokenizer.nextToken(); // Split the expression based on dot String[] expressionTokens = expression.split(DOT_EXPRESSION); // first token is the field String datasourceField = expressionTokens[0]; // check if it is a legal datasource property name if (!datasourcePropMap.containsValue(datasourceField)) { throw CustomConfigControllerException.exceptions .illegalDatasourceProperty(value, datasourceField); } for (int i = 1; i < expressionTokens.length; i++) { String[] methodTokens = StringUtils.split( expressionTokens[i], "[,()\"]"); // first token is the method name, remaining tokens are the // method arguments String stringMethod = methodTokens[0]; if (getCustomConfigMethod(stringMethod) == null) { throw CustomConfigControllerException.exceptions .illegalStringFunction(value, stringMethod); } } } } } /** * Custom name can have 1) Literals 2) Datasource fields and the operation * to be applied on them 3) String manipulation functions * * Datasource fields and the string manipulation functions will be within {} * * The custom name definition is expected to be in the format below: * {datafield1 * .operation(params..).operation(params..)}literal{datasource.field2 * .operation(params..).operation(params..)} Eg: * Brcd_{host_name.FIRST(8)}_{hba_port_wwn * .LAST(12)}_{array_port_wwn.LAST(12)}_{array_serial_number.LAST(8)} or Eg: * Brcd_ * ({cluster_name.FIRST(8)}_{host_name.LAST(12)}_{array_serial_number.LAST * (8)}).TRIM("_") * * @param value * - custom definition value * @param dataSource * - datasource property details * @return resolved name */ public String resolve(CustomConfigType configTemplate, StringMap scope, String value, DataSource dataSource) { IllegalCharsConstraint illegalCharConstraint = null; // Get the illegal char constraint to apply to the generated value from // datasource and string functions for (CustomConfigConstraint constraint : configTemplate .getConstraints()) { if (constraint instanceof IllegalCharsConstraint) { illegalCharConstraint = (IllegalCharsConstraint) constraint; break; } } // get systemType or global for use by constraints String systemType = CustomConfigConstants.DEFAULT_KEY; if (scope != null) { systemType = scope.get(CustomConfigConstants.SYSTEM_TYPE_SCOPE); // if the system type scope is not available, check for host type scope. // host type scope is only available for Hitachi Host Mode Option if (systemType == null) { systemType = scope.get(CustomConfigConstants.HOST_TYPE_SCOPE); } } // Look for expressions enclosed in parenthesis, the expression // does not include any methods for example if the expression is // ({cluster_name.FIRST(8)}_{host_name.LAST(12)}).TOLOWER(), this // function finds {cluster_name.FIRST(8)}_{host_name.LAST(12)} String subExp = getMostNestedExpression(value); String methodsRegex = getRegexWithMethodNames(METHODS_EXPRESSION); int loopCounter = 0; while (subExp != null && loopCounter < MAX_LOOP) { loopCounter++; String fullExpression = value; // get the sub-expression between parenthesis e.g. // {cluster_name.FIRST(8)}_{host_name.LAST(12)} logger.info("subExp is " + subExp); // get the full expression e.g. // ({cluster_name.FIRST(8)}_{host_name.LAST(12)}).TOLOWER() String subExpFull = "(" + subExp + ")"; Pattern p = Pattern.compile("(" + Pattern.quote(subExpFull) + "(" + methodsRegex + "))"); Matcher m = p.matcher(value); List<String> methods = new ArrayList<String>(); if (m.find()) { fullExpression = m.group(1); logger.info("full expression is " + fullExpression); // remove the sub expression from the full expression // and extract the methods from the full expression // m.group(2) should return .TOLOWER() String[] tokens = m.group(2).split(DOT_EXPRESSION); int size = tokens.length; if (size > 1) { // has the string methods add the first member as empty string, // since it is not used later. methods.add(" "); for (int i = 1; i < size; i++) { methods.add(tokens[i]); } } } // resolve the sub-expression String rsubExp = resolveSubExpression(subExp, dataSource, illegalCharConstraint, systemType); // execute the methods subExp = processStringMethods( methods.toArray(new String[methods.size()]), rsubExp); // replace the full nested expression with its value and repeat // looking for more value = value.replace(fullExpression, subExp); // repeat until all nested expressions are resolved subExp = getMostNestedExpression(value); } // After all nested expressions are resolved, do the first-level of // expressions value = resolveSubExpression(value, dataSource, illegalCharConstraint, systemType); // make sure we have a string that is at least 1 char long if (StringUtils.isEmpty(value.trim())) { throw CustomConfigControllerException.exceptions .resolvedCustomNameEmpty(configTemplate.getName()); } return value; } /** * Look for the most nested expressions first. */ private String getMostNestedExpression(String value) { String regex = getRegexWithMethodNames(NESTED_EXPRESSION_START); // I want the mosted nested expression so looking for the most nested ( // not proceeded // not proceeded with one of the methods. Say I start with: // Brcd_({vsan/fabric.FIRST(8)}_({array_serial_number.LAST(8)}_{host_name.FIRST(8)}).LAST(11)_{array_port_wwn.LAST(12)}).TRIM("_") // the first loop I get: // {vsan/fabric.FIRST(8)}_({array_serial_number.LAST(8)}_{host_name.FIRST(8)}).LAST(11)_{array_port_wwn.LAST(12)}).TRIM("_") // the second (and last) loop I get: // ({array_serial_number.LAST(8)}_{host_name.FIRST(8)}).LAST(11)_{array_port_wwn.LAST(12)}).TRIM("_") Pattern p = Pattern.compile(regex); Matcher m = p.matcher(value); String fullExpression = null; int loopCounter = 0; while (m.find() && loopCounter < MAX_LOOP) { // make sure we do not try more than MAX_LOOP loopCounter++; // get the substring that come after the parenthesis fullExpression = m.group(1); // check for the next parenthesis m = p.matcher(fullExpression); } // at this time full expression is : // ({array_serial_number.LAST(8)}_{host_name.FIRST(8)}).LAST(11)_{array_port_wwn.LAST(12)}).TRIM("_") // and it is the substring that starts with the most nest ( // use the full parse regex to get the actual expression // the next regex should get // ({array_serial_number.LAST(8)}_{host_name.FIRST(8)}).LAST(11) if (m != null && fullExpression != null && fullExpression.length() > 0) { int index = getClosingParenthesisIndex(fullExpression); if (index > 0) { return fullExpression.substring(0, index); } } return null; } private int getClosingParenthesisIndex(String fullExpression) { int c = 0; for (int i = 0; i < fullExpression.length(); i++) { if (fullExpression.charAt(i) == '(') { c++; } else if (fullExpression.charAt(i) == ')') { c--; } if (c < 0) { return i; } } return -1; } /** * Takes Resolved an expression in the form * Brcd_{host_name.FIRST(8)}_{hba_port_wwn * .LAST(12)}_{array_port_wwn.LAST(12)}_{array_serial_number.LAST(8)} * * @param value * the expression * @param dataSource * the object containing the value of the variables. * @param constraint * the illegal char constraint for this config type * @param systemType * the system type * @return the resolved expression */ private String resolveSubExpression(String value, DataSource dataSource, IllegalCharsConstraint constraint, String systemType) { StringBuffer resolvedValue = new StringBuffer(); StringTokenizer tokenizer = new StringTokenizer(value, START_DELIMITER + END_DELIMITER, true); while (tokenizer.hasMoreTokens()) { String token = tokenizer.nextToken(); if (token.equals(START_DELIMITER) && tokenizer.hasMoreTokens()) { // this is a datasource property which should be computed String propertyValue = getDataSourceValue( tokenizer.nextToken().replaceAll("\\s+", ""), dataSource, constraint, systemType); if (constraint != null) { propertyValue = constraint.applyConstraint(propertyValue, systemType); } resolvedValue.append(propertyValue); } else if (!token.equals(START_DELIMITER) && !token.equals(END_DELIMITER)) { // a literal - just append it resolvedValue.append(token); } } return resolvedValue.toString(); } /** * If the method names are not yet replaced, replace them. * * @return the regex expression with method names replaced * */ private String getRegexWithMethodNames(String expression) { if (expression.contains(METHOD_NAMES)) { String methods = StringUtils .join(getCustomConfigMethodNames(), "|"); expression = expression.replaceAll(METHOD_NAMES, methods); } return expression; } /** * Routine determines if the value looks invalid, like having too many parentheses or not enough. * * @param value [IN] - Configuration value to validate */ private void simpleSyntaxValidation(String value) { // Do a simple validation of value by tracking opening/closing brackets and parenthesis Map<String, Integer> characterCounts = new HashMap<>(); characterCounts.put(START_DELIMITER, 0); characterCounts.put(START_PAREN, 0); for (char theChar : value.toCharArray()) { if (START_DELIMITER.indexOf(theChar) != -1) { Integer count = characterCounts.get(START_DELIMITER); characterCounts.put(START_DELIMITER, count + 1); } else if (END_DELIMITER.indexOf(theChar) != -1) { Integer count = characterCounts.get(START_DELIMITER); characterCounts.put(START_DELIMITER, count - 1); } else if (START_PAREN.indexOf(theChar) != -1) { Integer count = characterCounts.get(START_PAREN); characterCounts.put(START_PAREN, count + 1); } else if (END_PAREN.indexOf(theChar) != -1) { Integer count = characterCounts.get(START_PAREN); characterCounts.put(START_PAREN, count - 1); } } // Any of the character counts not zero means there is too much // of it or not enough. String together the reason for (String character : characterCounts.keySet()) { Integer count = characterCounts.get(character); if (count != 0) { String invalid = ""; // Get the closing character ... if (START_DELIMITER.contains(character)) { invalid = END_DELIMITER; } else if (START_PAREN.contains(character)) { invalid = END_PAREN; } // Build the reason message String reason = ""; if (count > 0) { reason = String.format("Missing a '%s'", invalid); } else if (count < 0) { reason = String.format("Extra '%s' characters", invalid); } throw CustomConfigControllerException.exceptions.invalidSyntax(value, reason); } } } }