001 /** 002 * Copyright 2005-2014 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 */ 016 package org.kuali.rice.krad.uif.util; 017 018 import org.apache.commons.lang.StringUtils; 019 import org.apache.commons.logging.Log; 020 import org.apache.commons.logging.LogFactory; 021 import org.kuali.rice.core.api.exception.RiceRuntimeException; 022 import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean; 023 024 import java.util.ArrayList; 025 import java.util.HashMap; 026 import java.util.List; 027 import java.util.Map; 028 029 /** 030 * Utility class for UIF expressions 031 * 032 * @author Kuali Rice Team (rice.collab@kuali.org) 033 */ 034 public 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 expressions 039 * map for the expressionConfigurable or a nested expressionConfigurable (for the case of nested expression property names) 040 * 041 * <p> 042 * Expressions that are configured on properties and pulled out by the {@link org.kuali.rice.krad.uif.util.UifBeanFactoryPostProcessor} 043 * and put in the {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getExpressionGraph()} for the bean that is 044 * at root (non nested) level. Before evaluating the expressions, they need to be moved to the 045 * {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getPropertyExpressions()} map for the expressionConfigurable that 046 * property 047 * is on. 048 * </p> 049 * 050 * @param expressionConfigurable expressionConfigurable instance to process expressions for 051 * @param buildRefreshGraphs indicates whether the expression graphs for component refresh should be built 052 */ 053 public static void populatePropertyExpressionsFromGraph(UifDictionaryBean expressionConfigurable, boolean buildRefreshGraphs) { 054 if (expressionConfigurable == null || expressionConfigurable.getExpressionGraph() == null) { 055 return; 056 } 057 058 // will hold graphs to populate the refreshExpressionGraph property on each expressionConfigurable 059 // key is the path to the expressionConfigurable and value is the map of nested property names to expressions 060 Map<String, Map<String, String>> refreshExpressionGraphs = new HashMap<String, Map<String, String>>(); 061 062 Map<String, String> expressionGraph = expressionConfigurable.getExpressionGraph(); 063 for (Map.Entry<String, String> expressionEntry : expressionGraph.entrySet()) { 064 String propertyName = expressionEntry.getKey(); 065 String expression = expressionEntry.getValue(); 066 067 // by default assume expression belongs with passed in expressionConfigurable 068 UifDictionaryBean configurableWithExpression = expressionConfigurable; 069 070 // if property name is nested, we need to move the expression to the last expressionConfigurable 071 String adjustedPropertyName = propertyName; 072 if (StringUtils.contains(propertyName, ".")) { 073 String configurablePath = StringUtils.substringBeforeLast(propertyName, "."); 074 adjustedPropertyName = StringUtils.substringAfterLast(propertyName, "."); 075 076 Object nestedObject = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, configurablePath); 077 if ((nestedObject == null) || !(nestedObject instanceof UifDictionaryBean)) { 078 throw new RiceRuntimeException( 079 "Object for which expression is configured on is null or does not implement UifDictionaryBean: '" 080 + configurablePath 081 + "'"); 082 } 083 084 // use nested object as the expressionConfigurable which will get the property expression 085 configurableWithExpression = (UifDictionaryBean) nestedObject; 086 087 // now add the expression to the refresh graphs 088 if (buildRefreshGraphs) { 089 String currentPath = ""; 090 091 String[] configurablePathNames = StringUtils.split(configurablePath, "."); 092 for (String configurablePathName : configurablePathNames) { 093 if (StringUtils.isNotBlank(currentPath)) { 094 currentPath += "."; 095 } 096 currentPath += configurablePathName; 097 098 Map<String, String> graphExpressions = null; 099 if (refreshExpressionGraphs.containsKey(currentPath)) { 100 graphExpressions = refreshExpressionGraphs.get(currentPath); 101 } else { 102 graphExpressions = new HashMap<String, String>(); 103 refreshExpressionGraphs.put(currentPath, graphExpressions); 104 } 105 106 // property name in refresh graph should be relative to expressionConfigurable 107 String configurablePropertyName = StringUtils.substringAfter(propertyName, currentPath + "."); 108 graphExpressions.put(configurablePropertyName, expression); 109 } 110 } 111 } 112 113 configurableWithExpression.getPropertyExpressions().put(adjustedPropertyName, expression); 114 } 115 116 // set the refreshExpressionGraph property on each expressionConfigurable an expression was found for 117 if (buildRefreshGraphs) { 118 for (String configurablePath : refreshExpressionGraphs.keySet()) { 119 Object nestedObject = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, configurablePath); 120 // note if nested object is not a expressionConfigurable, then it can't be refresh and we can safely ignore 121 if ((nestedObject != null) && (nestedObject instanceof UifDictionaryBean)) { 122 ((UifDictionaryBean) nestedObject).setRefreshExpressionGraph(refreshExpressionGraphs.get( 123 configurablePath)); 124 } 125 } 126 127 // the expression graph for the passed in expressionConfigurable will be its refresh graph as well 128 expressionConfigurable.setRefreshExpressionGraph(expressionGraph); 129 } 130 } 131 132 /** 133 * Takes in an expression and a list to be filled in with names(property names) 134 * of controls found in the expression. This method returns a js expression which can 135 * be executed on the client to determine if the original exp was satisfied before 136 * interacting with the server - ie, this js expression is equivalent to the one passed in. 137 * 138 * There are limitations on the Spring expression language that can be used as this method. 139 * It is only used to parse expressions which are valid case statements for determining if 140 * some action/processing should be performed. ONLY Properties, comparison operators, booleans, 141 * strings, matches expression, and boolean logic are supported. Properties must 142 * be a valid property on the form, and should have a visible control within the view. 143 * 144 * Example valid exp: account.name == 'Account Name' 145 * 146 * @param exp 147 * @param controlNames 148 * @return 149 */ 150 public static String parseExpression(String exp, List<String> controlNames) { 151 // clean up expression to ease parsing 152 exp = exp.trim(); 153 if (exp.startsWith("@{")) { 154 exp = StringUtils.removeStart(exp, "@{"); 155 if (exp.endsWith("}")) { 156 exp = StringUtils.removeEnd(exp, "}"); 157 } 158 } 159 160 exp = StringUtils.replace(exp, "!=", " != "); 161 exp = StringUtils.replace(exp, "==", " == "); 162 exp = StringUtils.replace(exp, ">", " > "); 163 exp = StringUtils.replace(exp, "<", " < "); 164 exp = StringUtils.replace(exp, "<=", " <= "); 165 exp = StringUtils.replace(exp, ">=", " >= "); 166 167 String conditionJs = exp; 168 String stack = ""; 169 170 boolean expectingSingleQuote = false; 171 boolean ignoreNext = false; 172 for (int i = 0; i < exp.length(); i++) { 173 char c = exp.charAt(i); 174 if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) { 175 evaluateCurrentStack(stack.trim(), controlNames); 176 //reset stack 177 stack = ""; 178 continue; 179 } else if (!ignoreNext && c == '\'') { 180 stack = stack + c; 181 expectingSingleQuote = !expectingSingleQuote; 182 } else if (c == '\\') { 183 stack = stack + c; 184 ignoreNext = !ignoreNext; 185 } else { 186 stack = stack + c; 187 ignoreNext = false; 188 } 189 } 190 191 if (StringUtils.isNotEmpty(stack)) { 192 evaluateCurrentStack(stack.trim(), controlNames); 193 } 194 195 conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll( 196 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ") 197 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s", 198 " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll( 199 "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)").replaceAll("\\s?(?i:#listContains)\\((.*?)\\)", 200 "listContains($1)").replaceAll("\\s?(?i:#emptyList)\\((.*?)\\)", "emptyList($1)"); 201 202 if (conditionJs.contains("matches")) { 203 conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null "); 204 conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/"); 205 conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)"); 206 } 207 208 List<String> removeControlNames = new ArrayList<String>(); 209 List<String> addControlNames = new ArrayList<String>(); 210 //convert property names to use coerceValue function and convert arrays to js arrays 211 for (String propertyName : controlNames) { 212 //array definitions are caught in controlNames because of the nature of the parse - convert them and remove 213 if(propertyName.trim().startsWith("{") && propertyName.trim().endsWith("}")){ 214 String array = propertyName.trim().replace('{', '['); 215 array = array.replace('}', ']'); 216 conditionJs = conditionJs.replace(propertyName, array); 217 removeControlNames.add(propertyName); 218 continue; 219 } 220 221 //handle not 222 if (propertyName.startsWith("!")){ 223 String actualPropertyName = StringUtils.removeStart(propertyName, "!"); 224 conditionJs = conditionJs.replace(propertyName, 225 "!coerceValue(\"" + actualPropertyName + "\")"); 226 removeControlNames.add(propertyName); 227 addControlNames.add(actualPropertyName); 228 } 229 else{ 230 conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")"); 231 } 232 } 233 234 controlNames.removeAll(removeControlNames); 235 controlNames.addAll(addControlNames); 236 237 return conditionJs; 238 } 239 240 /** 241 * Used internally by parseExpression to evalute if the current stack is a property 242 * name (ie, will be a control on the form) 243 * 244 * @param stack 245 * @param controlNames 246 */ 247 public static void evaluateCurrentStack(String stack, List<String> controlNames) { 248 if (StringUtils.isNotBlank(stack)) { 249 if (!(stack.equals("==") 250 || stack.equals("!=") 251 || stack.equals(">") 252 || stack.equals("<") 253 || stack.equals(">=") 254 || stack.equals("<=") 255 || stack.equalsIgnoreCase("ne") 256 || stack.equalsIgnoreCase("eq") 257 || stack.equalsIgnoreCase("gt") 258 || stack.equalsIgnoreCase("lt") 259 || stack.equalsIgnoreCase("lte") 260 || stack.equalsIgnoreCase("gte") 261 || stack.equalsIgnoreCase("matches") 262 || stack.equalsIgnoreCase("null") 263 || stack.equalsIgnoreCase("false") 264 || stack.equalsIgnoreCase("true") 265 || stack.equalsIgnoreCase("and") 266 || stack.equalsIgnoreCase("or") 267 || stack.contains("#empty") 268 || stack.equals("!") 269 || stack.contains("#emptyList") 270 || stack.contains("#listContains") 271 || stack.startsWith("'") 272 || stack.endsWith("'"))) { 273 274 boolean isNumber = false; 275 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) { 276 try { 277 Double.parseDouble(stack); 278 isNumber = true; 279 } catch (NumberFormatException e) { 280 isNumber = false; 281 } 282 } 283 284 if (!(isNumber)) { 285 //correct argument of a custom function ending in comma 286 if(StringUtils.endsWith(stack, ",")){ 287 stack = StringUtils.removeEnd(stack, ",").trim(); 288 } 289 290 if (!controlNames.contains(stack)) { 291 controlNames.add(stack); 292 } 293 } 294 } 295 } 296 } 297 298 }