/* * OpenClinica is distributed under the * GNU Lesser General Public License (GNU LGPL). * For details see: http://www.openclinica.org/license * copyright 2003-2005 Akaza Research */ package org.akaza.openclinica.logic.score; /** * Provides some validation methods for scoring. * * Here are some definitions: * <ul> * <li>'term' is one string segment. It can be an expression, a formula, one * argument of a formula, a variable, a numbers. * <li>'expression' is a math expression contains arithmetic operators, * formulae, variables, numbers. * <li>'formula' contains arguments. * <li>'argument' may be an expression, a variable, a number * </ul> * * * @author ywang (Jan. 2008) */ import java.util.ArrayList; import java.util.Locale; import java.util.ResourceBundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class ScoreValidator { private Locale locale; private ResourceBundle resexception; protected final Logger logger = LoggerFactory.getLogger(getClass().getName()); public ScoreValidator(Locale locale) { this.locale = locale; this.resexception = ResourceBundle.getBundle("org.akaza.openclinica.i18n.exceptions", locale); } public void setLocale(Locale l) { this.locale = l; } public Locale getLocale() { return this.locale; } /** * Return true is an expression has valid syntax.<br> * For decode function, now, only validate the very first argument.<br> * Supported operators include + * - / and ( ) , * * @param expression * @param errors * @return */ public boolean isValidExpression(String expression, StringBuffer errors, ArrayList<String> allVariables) { if (expression == null || expression.length() < 1) { // errors.append("Expression is empty" + "; "); errors.append(resexception.getString("expression_is_empty") + "; "); return false; } // process the prefix String exp = expression; if (exp.startsWith("func:")) { exp = exp.substring(5).trim(); } // adding get external here, tbh, 05/2009 if (exp.contains("getexternalvalue") || exp.contains("getExternalValue")) { // System.out.println("^^^ got to first error block ^^^"); errors = processExternalValues(exp); if (errors != null && errors.length() > 1) return false; return true; } // end, tbh 05/2009 // get rid of space in exp exp = exp.replace(" ", ""); exp = exp.replaceAll("##", ","); String token = ""; String finalExpression = ""; ScoreUtil.Info info = new ScoreUtil.Info(); info.pos = 0; info.level = 0; StringBuffer err = new StringBuffer(); char contents[] = exp.toCharArray(); int length = exp.length(); char tempnext = info.pos < contents.length - 1 ? contents[info.pos + 1] : ' '; if (!isValidSign(contents[0], tempnext)) { if (!isValidExpStart(contents[0])) { // errors.append("Expression can not start with" + " " + // contents[0] + "; "); errors.append(resexception.getString("expression_cannot_start_with") + " " + contents[0] + "; "); } } if (!isValidExpEnd(contents[length - 1])) { // errors.append("Expression can not end with" + " " + // contents[length-1] + "; "); errors.append(resexception.getString("expression_cannot_end_with") + " " + contents[0] + "; "); } while (info.pos < contents.length) { char c = contents[info.pos]; char next = info.pos < contents.length - 1 ? contents[info.pos + 1] : ' '; // we ignore spaces if (c == ' ') { // do nothing } else if (ScoreUtil.isOperator(c)) { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } token = token.trim(); if (token.length() > 0 && !isNumber(token) && !allVariables.contains(token)) { allVariables.add(token); } finalExpression += token + c; token = ""; } else if (c == '(') { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } if (isSupportedFunc(token)) { err.delete(0, err.length()); if (!isValidFunction(contents, info, token, err, allVariables)) { errors.append(err); } // fake a result to carry on syntax check finalExpression += "0"; } else { if (token.length() > 1) { // errors.append(token + " " + "is not a valid function // or has not been supported yet" +"; "); // System.out.println("error on line 139"); errors.append(token + " " + resexception.getString("function_is_invalid_or_not_supported") + "; "); // carry on syntax check err.delete(0, err.length()); ArrayList<String> variables = new ArrayList<String>(); if (!isValidFunction(contents, info, token, err, variables)) { errors.append(err); } // fake a function result to carry on syntax check finalExpression += "0"; } else { // just append it then info.level++; finalExpression += token + c; } } token = ""; } else if (c == ')') { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } token = token.trim(); if (token.length() > 0 && !isNumber(token) && !allVariables.contains(token)) { allVariables.add(token); } finalExpression += token + c; token = ""; info.level--; } else if (c == ',') { // errors.append("One comma is invalid"+"; "); errors.append(resexception.getString("one_comma_invalid") + "; "); token = ""; // this should not happen in this method since // getFunctionValue() // is taking care of of the function } else { // otherwise, we just append this character to the token token += c; } char nextnext = info.pos < contents.length - 2 ? contents[info.pos + 2] : ' '; boolean isNextSign = isValidSign(next, nextnext); if (!isNextSign) { if (!isValidOrder(c, next)) { // errors.append("The character"+" " + c + " " + "should not // be followed by the character " + next + "; "); // System.out.println("hit this error instead: " + c + next); errors.append(resexception.getString("the_character") + " " + c + " " + resexception.getString("should_not_followed_by_character") + " " + next + "; "); } } info.pos++; } if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } if (info.level != 0) { // errors.append("Expression"+" " + exp + " "+"is invalid because // parenthesises are not correctly paired"+"; "); errors.append(resexception.getString("expression") + " " + exp + " " + resexception.getString("is_invalid_because_wrong_paired_parenthesises") + "; "); } finalExpression += token; if (errors != null && errors.length() > 1) return false; return true; } /** * process the HTML and whether or not all four values are valid, tbh 05/2009 */ private StringBuffer processExternalValues(String expression) { expression = expression.replace(" ", ""); StringBuffer errors = new StringBuffer(); String[] values = expression.split("##"); String leftright = values[1]; if (!leftright.equalsIgnoreCase("left") && !leftright.equalsIgnoreCase("right")) { errors.append("Your expression in getExternalValues is incorrect: the second value should be 'right' or 'left', not '" + leftright + "'; "); } String height = values[2]; logger.debug("found height: " + height); String width = values[3]; try { Integer neightInt = new Integer(height); } catch (NumberFormatException npe) { errors.append("Your expression in getExternalValues is incorrect: the third value should be a number, not '" + height + "'; "); } width = width.replace(")", ""); logger.debug("found width: " + width); try { Integer widthInt = new Integer(width); } catch (NumberFormatException npe) { errors.append("Your expression in getExternalValues is incorrect: the fourth value should be a number, not '" + width + "'; "); } // checking three values: left/right, and two ints return errors; } public boolean isValidFunction(char[] contents, ScoreUtil.Info info, String func, StringBuffer errors, ArrayList<String> allVariables) { int originalLevel = info.level; info.pos++; info.level++; String token = ""; // the currentExpression is in fact representing the current argument. // Here, each argument is treated individually, one at a time. String currentExpression = ""; int argCount = 0; StringBuffer err = new StringBuffer(); ArrayList<String> funcVariables = new ArrayList<String>(); while (info.pos < contents.length) { char c = contents[info.pos]; char next = info.pos < contents.length - 1 ? contents[info.pos + 1] : ' '; if (c == ')') { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } info.level--; // end of the function, marked by the equal level if (info.level == originalLevel) { currentExpression += token; err.delete(0, err.length()); if (!isValidArgument(currentExpression, err, funcVariables)) { // errors.append("One argument of formula '" + func + "' // is invalid; "); errors.append(err); } token = ""; break; } else { // end of an expression, just store them in the current // argument currentExpression += token + c; } token = ""; } else if (c == '(') { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } // again, it is either the start of a function or an expression if (token != null && isSupportedFunc(token)) { err.delete(0, err.length()); ArrayList<String> variables = new ArrayList<String>(); if (!isValidFunction(contents, info, token, err, variables)) { errors.append(err); } if (variables.size() > 0) { if (token.equalsIgnoreCase("decode") && !allVariables.contains(variables.get(0))) allVariables.add(variables.get(0)); else { for (String s : variables) { if (s.length() > 0 && !allVariables.contains(s)) { allVariables.add(s); } } } } // fake a function result to store in the current argument // and carry on syntax checking currentExpression += "0"; }// if it is the start of an expression else { if (token.length() > 1) { // errors.append(token + " is not a valid function or // has not been supported yet"+"; "); // System.out.println("found error on line 287"); errors.append(token + " " + resexception.getString("function_is_invalid_or_not_supported") + "; "); // carry on syntax check err.delete(0, err.length()); ArrayList<String> variables = new ArrayList<String>(); if (!isValidFunction(contents, info, token, err, variables)) { errors.append(err); } // fake a function result to carry on syntax check currentExpression += "0"; if (variables.size() > 0) { for (String s : variables) { if (s.length() > 0 && !allVariables.contains(s)) { allVariables.add(s); } } } } else { info.level++; currentExpression += token + c; } } token = ""; }// end of an argument else if (c == ',') { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } token = token.trim(); if (token.length() > 0 && !isNumber(token) && !funcVariables.contains(token)) { funcVariables.add(token); } currentExpression += token; err.delete(0, err.length()); if (!isValidArgument(currentExpression, err, funcVariables)) { errors.append(err); } ++argCount; token = ""; currentExpression = ""; } else if (ScoreUtil.isOperator(c)) { if (!noCommaEnds(token)) { // errors.append(token + " should not contain comma" +"; "); errors.append(token + " " + resexception.getString("should_not_contain_comma") + "; "); } token = token.trim(); if (token.length() > 0 && !isNumber(token) && !funcVariables.contains(token)) { funcVariables.add(token); } currentExpression += token + c; token = ""; } else { if (c != ' ') { token += c; } } char nextnext = info.pos < contents.length - 2 ? contents[info.pos + 2] : ' '; boolean isNextSign = isValidSign(next, nextnext, func); if (!isNextSign) { // System.out.println("got this far: func: " + func); if (!isValidOrder(c, next, func)) { //&& !isValidUrl(contents, token, func)) { // errors.append("One character"+" " + c + " "+"has been // followed by a wrong character"+" " + next + "; "); errors.append(resexception.getString("the_character") + " " + c + " " + resexception.getString("should_not_followed_by_character") + " " + next + "; "); } } info.pos++; } ++argCount; if (isTwoArgs(func) && argCount != 2) { // errors.append("Function"+" " + func + "() should have 2 arguments // only"+"; "); errors.append(resexception.getString("function") + " " + func + " " + resexception.getString("should_have_2_arguments_only") + "; "); } if (func.equalsIgnoreCase("decode")) { String s = funcVariables.get(0).trim(); if (s.length() > 0 && !allVariables.contains(s)) { allVariables.add(funcVariables.get(0)); } } else { for (String s : funcVariables) { if (s.length() > 0 && !allVariables.contains(s)) { allVariables.add(s); } } } if (errors != null && errors.length() > 1) return false; return true; } /* * checks to make sure we have a valid URL for the external-value function. tbh, 05/2009 */ public boolean isValidUrl(char[] contents, String token, String func) { // System.out.println("found contents: " + contents.toString() + " for token: " + token + " and for func: " + func); if (func.equalsIgnoreCase("getexternalvalue"))//(contents.toString().startsWith("http://")) return true; // TODO replace with regexp in future versions return false; } public boolean isValidArgument(String term, StringBuffer errors, ArrayList<String> allVariables) { if (isNumber(term)) { return true; } else if (isExpression(term)) { return isValidExpression(term, errors, allVariables); } else { // if(!isNumber(term)) allVariables.add(term); term = term.trim(); if (term.length() > 0 && !allVariables.contains(term)) { allVariables.add(term); } return true; } } /** * Return true if an expression does not start with arithmetic operators, * ')', ',', '.' * * @param ch * @return */ public static boolean isValidExpStart(char ch) { return !(ScoreUtil.isOperator(ch) || ch == ')' || ch == ',' || ch == '.'); } /** * Return true if an expression does not end with arithmetic operators, '(', * ',', '.' * * @param ch * @return */ public static boolean isValidExpEnd(char ch) { return !(ScoreUtil.isOperator(ch) || ch == '(' || ch == ',' || ch == '.'); } /** * Function, variable, argument can not start and/or end with '.' * * <br> * This method only checks '.' * * @param term * @return */ public static boolean noCommaEnds(String term) { return !(term.startsWith(".") || term.endsWith(".")); } /** * This method can be used when a character can possibly be a sign. It * checks next following character. Return true if the target character is a * sign. No space exists between two characters. * * @param ch * @param next * @return */ public static boolean isValidSign(char ch, char next, String function) { if (ch == '-' || ch == '+') { if (!ScoreUtil.isOperator(next) && next != ')' && next != ',') { return true; } } // break to exclude URLs, to fix, tbh 05/2009 if (ch == '/') { if ((next == '/') && (function.equalsIgnoreCase("getexternalvalue"))) { return true; } } return false; } public static boolean isValidSign(char ch, char next) { return isValidSign(ch, next, ""); } /** * Return false if current character has been followed by a illegal * character, e.g., it will return false if ',' has been followed by ')' * * <p> * This method only checks + - * / ( ) and ,<br> + and - are operators * instead of signs<br> * No space between two characters<br> * </p> * * @param curr * @param next * @return */ public static boolean isValidOrder(char curr, char next) { return isValidOrder(curr, next, ""); } public static boolean isValidOrder(char curr, char next, String func) { if (curr == '/' && next == '/' && func.equalsIgnoreCase("getexternalvalue")) { // escaping urls, tbh 05/2009 return true; } if (curr == '(') { if (next == ')' || ScoreUtil.isOperator(next) || next == ',') return false; } else if (curr == ')') { if (next != ',' && next != ')' && !ScoreUtil.isOperator(next) && next != ' ') return false; } else if (curr == ',') { if (next == ',' || ScoreUtil.isOperator(next) || next == ')') return false; } else if (ScoreUtil.isOperator(curr)) { if (next == ')' || next == ',' || ScoreUtil.isOperator(next)) return false; } return true; } /* * Return true if a string matches those formulae which have been supported * for scoring calculation: <br>avg(), max(), min(), median(), pow(), * stdev(), sum * * @param token @return */ /* * public static boolean isCalcFuncName(String token) { return * token.equalsIgnoreCase("sum") || token.equalsIgnoreCase("avg") || * token.equalsIgnoreCase("min") || token.equalsIgnoreCase("max") || * token.equalsIgnoreCase("median") || token.equalsIgnoreCase("pow") || * token.equalsIgnoreCase("stdev"); } */ /** * Return true if a string matches one name of functions that have been * supported. * * @param token * @return */ public static boolean isSupportedFunc(String token) { return token.equalsIgnoreCase("sum") || token.equalsIgnoreCase("avg") || token.equalsIgnoreCase("min") || token.equalsIgnoreCase("max") || token.equalsIgnoreCase("median") || token.equalsIgnoreCase("pow") || token.equalsIgnoreCase("stdev") || token.equalsIgnoreCase("decode") || token.equalsIgnoreCase("getexternalvalue"); } /** * Return true if a string contains at least one of those characters: '+', * '-', '*', '/', '(', ')', ',' * * @param term * @return */ public static boolean isExpression(String term) { return term.contains("+") || term.contains("-") || term.contains("*") || term.contains("/") || term.contains("(") || term.contains(")") || term.contains(","); } /** * Return true if a function belongs to below supported function(s) which * allow(s) only two arguments: * <li> pow() * * * @param functionName * @return */ public static boolean isTwoArgs(String functionName) { return functionName.equalsIgnoreCase("pow"); } public static boolean isNumber(String variable) { try { Double d = Double.parseDouble(variable); } catch (Exception e) { return false; } return true; } }