001/** 002 * Copyright 2005-2018 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.uif.util; 017 018import org.apache.commons.lang.StringUtils; 019import org.apache.commons.logging.Log; 020import org.apache.commons.logging.LogFactory; 021import org.kuali.rice.core.api.exception.RiceRuntimeException; 022import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean; 023 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029/** 030 * Utility class for UIF expressions 031 * 032 * @author Kuali Rice Team (rice.collab@kuali.org) 033 */ 034public class ExpressionUtils { 035 private static final Log LOG = LogFactory.getLog(ExpressionUtils.class); 036 037 /** 038 * Pulls expressions within the expressionConfigurable's expression graph and moves them to the property 039 * expressions 040 * map for the expressionConfigurable or a nested expressionConfigurable (for the case of nested expression 041 * property 042 * names) 043 * 044 * <p> 045 * Expressions that are configured on properties and pulled out by the {@link org.kuali.rice.krad.datadictionary.uif.UifBeanFactoryPostProcessor} 046 * and put in the {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getExpressionGraph()} for the 047 * bean 048 * that is 049 * at root (non nested) level. Before evaluating the expressions, they need to be moved to the 050 * {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getPropertyExpressions()} map for the 051 * expressionConfigurable that 052 * property 053 * is on. 054 * </p> 055 * 056 * @param expressionConfigurable expressionConfigurable instance to process expressions for 057 */ 058 public static void populatePropertyExpressionsFromGraph(UifDictionaryBean expressionConfigurable) { 059 if (expressionConfigurable == null || expressionConfigurable.getExpressionGraph() == null) { 060 return; 061 } 062 063 // will hold graphs to populate the refreshExpressionGraph property on each expressionConfigurable 064 // key is the path to the expressionConfigurable and value is the map of nested property names to expressions 065 Map<String, Map<String, String>> refreshExpressionGraphs = new HashMap<String, Map<String, String>>(); 066 067 Map<String, String> expressionGraph = expressionConfigurable.getExpressionGraph(); 068 for (Map.Entry<String, String> expressionEntry : expressionGraph.entrySet()) { 069 String propertyName = expressionEntry.getKey(); 070 String expression = expressionEntry.getValue(); 071 072 // by default assume expression belongs with passed in expressionConfigurable 073 UifDictionaryBean configurableWithExpression = expressionConfigurable; 074 075 // if property name is nested, we need to move the expression to the last expressionConfigurable 076 String adjustedPropertyName = propertyName; 077 if (StringUtils.contains(propertyName, ".")) { 078 String configurablePath = StringUtils.substringBeforeLast(propertyName, "."); 079 adjustedPropertyName = StringUtils.substringAfterLast(propertyName, "."); 080 081 Object nestedObject = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, configurablePath); 082 083 // skip missing expression object for components skipping their lifecycle because objects 084 // in these components may be missing (and are expected to be missing) 085 if (nestedObject == null 086 && expressionConfigurable instanceof LifecycleElement 087 && ((LifecycleElement) expressionConfigurable).skipLifecycle()) { 088 continue; 089 } 090 091 if ((nestedObject == null) || !(nestedObject instanceof UifDictionaryBean)) { 092 throw new RiceRuntimeException("Object for which expression is configured on is null or does not " 093 + "implement UifDictionaryBean: '" 094 + configurablePath 095 + "' on class " 096 + expressionConfigurable.getClass().getName() 097 + " while evaluating " 098 + "expression for " 099 + propertyName); 100 } 101 102 // use nested object as the expressionConfigurable which will get the property expression 103 configurableWithExpression = (UifDictionaryBean) nestedObject; 104 } 105 106 configurableWithExpression.getPropertyExpressions().put(adjustedPropertyName, expression); 107 } 108 } 109 110 /** 111 * Takes in an expression and a list to be filled in with names(property names) 112 * of controls found in the expression. This method returns a js expression which can 113 * be executed on the client to determine if the original exp was satisfied before 114 * interacting with the server - ie, this js expression is equivalent to the one passed in. 115 * 116 * There are limitations on the Spring expression language that can be used as this method. 117 * It is only used to parse expressions which are valid case statements for determining if 118 * some action/processing should be performed. ONLY Properties, comparison operators, booleans, 119 * strings, matches expression, and boolean logic are supported. Properties must 120 * be a valid property on the form, and should have a visible control within the view. 121 * 122 * Example valid exp: account.name == 'Account Name' 123 * 124 * @param exp 125 * @param controlNames 126 * @return parsed expression, expressed as JS for client side evaluation 127 */ 128 public static String parseExpression(String exp, List<String> controlNames) { 129 // clean up expression to ease parsing 130 exp = exp.trim(); 131 if (exp.startsWith("@{")) { 132 exp = StringUtils.removeStart(exp, "@{"); 133 if (exp.endsWith("}")) { 134 exp = StringUtils.removeEnd(exp, "}"); 135 } 136 } 137 138 exp = StringUtils.replace(exp, "!=", " != "); 139 exp = StringUtils.replace(exp, "==", " == "); 140 exp = StringUtils.replace(exp, ">", " > "); 141 exp = StringUtils.replace(exp, "<", " < "); 142 exp = StringUtils.replace(exp, "<=", " <= "); 143 exp = StringUtils.replace(exp, ">=", " >= "); 144 145 String conditionJs = exp; 146 String stack = ""; 147 148 boolean expectingSingleQuote = false; 149 boolean ignoreNext = false; 150 for (int i = 0; i < exp.length(); i++) { 151 char c = exp.charAt(i); 152 if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) { 153 evaluateCurrentStack(stack.trim(), controlNames); 154 //reset stack 155 stack = ""; 156 continue; 157 } else if (!ignoreNext && c == '\'') { 158 stack = stack + c; 159 expectingSingleQuote = !expectingSingleQuote; 160 } else if (c == '\\') { 161 stack = stack + c; 162 ignoreNext = !ignoreNext; 163 } else { 164 stack = stack + c; 165 ignoreNext = false; 166 } 167 } 168 169 if (StringUtils.isNotEmpty(stack)) { 170 evaluateCurrentStack(stack.trim(), controlNames); 171 } 172 173 conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll( 174 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ") 175 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s", 176 " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll( 177 "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)").replaceAll( 178 "\\s?(?i:#listContains)\\((.*?)\\)", "listContains($1)").replaceAll( 179 "\\s?(?i:#emptyList)\\((.*?)\\)", "emptyList($1)"); 180 181 if (conditionJs.contains("matches")) { 182 conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null "); 183 conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/"); 184 conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)"); 185 } 186 187 List<String> removeControlNames = new ArrayList<String>(); 188 List<String> addControlNames = new ArrayList<String>(); 189 //convert property names to use coerceValue function and convert arrays to js arrays 190 for (String propertyName : controlNames) { 191 //array definitions are caught in controlNames because of the nature of the parse - convert them and remove 192 if (propertyName.trim().startsWith("{") && propertyName.trim().endsWith("}")) { 193 String array = propertyName.trim().replace('{', '['); 194 array = array.replace('}', ']'); 195 conditionJs = conditionJs.replace(propertyName, array); 196 removeControlNames.add(propertyName); 197 continue; 198 } 199 200 //handle not 201 if (propertyName.startsWith("!")) { 202 String actualPropertyName = StringUtils.removeStart(propertyName, "!"); 203 conditionJs = conditionJs.replace(propertyName, "!coerceValue(\"" + actualPropertyName + "\")"); 204 removeControlNames.add(propertyName); 205 addControlNames.add(actualPropertyName); 206 } else { 207 conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")"); 208 } 209 } 210 211 controlNames.removeAll(removeControlNames); 212 controlNames.addAll(addControlNames); 213 214 return conditionJs; 215 } 216 217 /** 218 * Used internally by parseExpression to evalute if the current stack is a property 219 * name (ie, will be a control on the form) 220 * 221 * @param stack 222 * @param controlNames 223 */ 224 public static void evaluateCurrentStack(String stack, List<String> controlNames) { 225 if (StringUtils.isNotBlank(stack)) { 226 if (!(stack.equals("==") 227 || stack.equals("!=") 228 || stack.equals(">") 229 || stack.equals("<") 230 || stack.equals(">=") 231 || stack.equals("<=") 232 || stack.equalsIgnoreCase("ne") 233 || stack.equalsIgnoreCase("eq") 234 || stack.equalsIgnoreCase("gt") 235 || stack.equalsIgnoreCase("lt") 236 || stack.equalsIgnoreCase("lte") 237 || stack.equalsIgnoreCase("gte") 238 || stack.equalsIgnoreCase("matches") 239 || stack.equalsIgnoreCase("null") 240 || stack.equalsIgnoreCase("false") 241 || stack.equalsIgnoreCase("true") 242 || stack.equalsIgnoreCase("and") 243 || stack.equalsIgnoreCase("or") 244 || stack.contains("#empty") 245 || stack.equals("!") 246 || stack.contains("#emptyList") 247 || stack.contains("#listContains") 248 || stack.startsWith("'") 249 || stack.endsWith("'"))) { 250 251 boolean isNumber = false; 252 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) { 253 try { 254 Double.parseDouble(stack); 255 isNumber = true; 256 } catch (NumberFormatException e) { 257 isNumber = false; 258 } 259 } 260 261 if (!(isNumber)) { 262 //correct argument of a custom function ending in comma 263 if (StringUtils.endsWith(stack, ",")) { 264 stack = StringUtils.removeEnd(stack, ",").trim(); 265 } 266 267 if (!controlNames.contains(stack)) { 268 controlNames.add(stack); 269 } 270 } 271 } 272 } 273 } 274 275}