001/** 002 * Copyright 2005-2016 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.kuali.rice.krad.uif.UifConstants; 020import org.kuali.rice.krad.uif.UifPropertyPaths; 021import org.kuali.rice.krad.uif.container.CollectionGroup; 022import org.kuali.rice.krad.uif.field.DataField; 023import org.kuali.rice.krad.uif.view.View; 024import org.kuali.rice.krad.uif.component.BindingInfo; 025import org.kuali.rice.krad.uif.component.Component; 026import org.kuali.rice.krad.uif.layout.LayoutManager; 027 028import java.util.HashMap; 029import java.util.List; 030import java.util.Map; 031 032/** 033 * Utility class for UIF expressions 034 * 035 * @author Kuali Rice Team (rice.collab@kuali.org) 036 */ 037public class ExpressionUtils { 038 039 /** 040 * Adjusts the property expressions for a given object. Any nested properties are moved to the parent 041 * object. Binding adjust prefixes are replaced with the correct values. 042 * 043 * <p> 044 * The org.kuali.rice.krad.uif.UifConstants#NO_BIND_ADJUST_PREFIX prefix will be removed 045 * as this is a placeholder indicating that the property is directly on the form. 046 * The org.kuali.rice.krad.uif.UifConstants#FIELD_PATH_BIND_ADJUST_PREFIX prefix will be replaced by 047 * the object's field path - this is only applicable to DataFields. The 048 * org.kuali.rice.krad.uif.UifConstants#DEFAULT_PATH_BIND_ADJUST_PREFIX prefix will be replaced 049 * by the view's default path if it is set. 050 * </p> 051 * 052 * @param view - the parent view of the object 053 * @param object - Object to adjust property expressions on 054 */ 055 public static void adjustPropertyExpressions(View view, Object object) { 056 if (object == null) { 057 return; 058 } 059 060 // get the map of property expressions to adjust 061 Map<String, String> propertyExpressions = new HashMap<String, String>(); 062 if (Component.class.isAssignableFrom(object.getClass())) { 063 propertyExpressions = ((Component) object).getPropertyExpressions(); 064 } else if (LayoutManager.class.isAssignableFrom(object.getClass())) { 065 propertyExpressions = ((LayoutManager) object).getPropertyExpressions(); 066 } else if (BindingInfo.class.isAssignableFrom(object.getClass())) { 067 propertyExpressions = ((BindingInfo) object).getPropertyExpressions(); 068 } 069 070 Map<String, String> adjustedPropertyExpressions = new HashMap<String, String>(); 071 for (Map.Entry<String, String> propertyExpression : propertyExpressions.entrySet()) { 072 String propertyName = propertyExpression.getKey(); 073 String expression = propertyExpression.getValue(); 074 075 // if property name is nested, need to move the expression to the parent object 076 if (StringUtils.contains(propertyName, ".")) { 077 boolean expressionMoved = moveNestedPropertyExpression(object, propertyName, expression); 078 079 // if expression moved, skip rest of control statement so it is not added to the adjusted map 080 if (expressionMoved) { 081 continue; 082 } 083 } 084 085 // replace the binding prefixes 086 String adjustedExpression = replaceBindingPrefixes(view, object, expression); 087 088 adjustedPropertyExpressions.put(propertyName, adjustedExpression); 089 } 090 091 // update property expressions map on object 092 ObjectPropertyUtils.setPropertyValue(object, UifPropertyPaths.PROPERTY_EXPRESSIONS, 093 adjustedPropertyExpressions); 094 } 095 096 /** 097 * Adjusts the property expressions for a given object 098 * 099 * <p> 100 * The org.kuali.rice.krad.uif.UifConstants#NO_BIND_ADJUST_PREFIX prefix will be removed 101 * as this is a placeholder indicating that the property is directly on the form. 102 * The org.kuali.rice.krad.uif.UifConstants#FIELD_PATH_BIND_ADJUST_PREFIX prefix will be replaced by 103 * the object's field path - this is only applicable to DataFields. The 104 * org.kuali.rice.krad.uif.UifConstants#DEFAULT_PATH_BIND_ADJUST_PREFIX prefix will be replaced 105 * by the view's default path if it is set. 106 * </p> 107 * 108 * @param view - the parent view of the object 109 * @param object - Object to adjust property expressions on 110 * @param expression - The expression to adjust 111 * @return the adjusted expression String 112 */ 113 public static String replaceBindingPrefixes(View view, Object object, String expression) { 114 String adjustedExpression = StringUtils.replace(expression, UifConstants.NO_BIND_ADJUST_PREFIX, ""); 115 116 // replace the field path prefix for DataFields 117 if (object instanceof DataField) { 118 119 // Get the binding path from the object 120 BindingInfo bindingInfo = ((DataField) object).getBindingInfo(); 121 String fieldPath = bindingInfo.getBindingPath(); 122 123 // Remove the property name from the binding path 124 fieldPath = StringUtils.removeEnd(fieldPath, "." + bindingInfo.getBindingName()); 125 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX, 126 fieldPath + "."); 127 } else { 128 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX, 129 ""); 130 } 131 132 // replace the default path prefix if there is one set on the view 133 if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) { 134 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX, 135 view.getDefaultBindingObjectPath() + "."); 136 137 } else { 138 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX, 139 ""); 140 } 141 142 // replace line path binding prefix with the actual line path 143 if (adjustedExpression.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) { 144 String linePath = getLinePathPrefixValue((Component) object); 145 146 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX, 147 linePath + "."); 148 } 149 150 // replace node path binding prefix with the actual node path 151 if (adjustedExpression.contains(UifConstants.NODE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) { 152 String nodePath = ""; 153 154 Map<String, Object> context = ((Component) object).getContext(); 155 if (context.containsKey(UifConstants.ContextVariableNames.NODE_PATH)) { 156 nodePath = (String) context.get(UifConstants.ContextVariableNames.NODE_PATH); 157 } 158 159 adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.NODE_PATH_BIND_ADJUST_PREFIX, 160 nodePath + "."); 161 } 162 163 return adjustedExpression; 164 } 165 166 /** 167 * Determines the value for the org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX binding prefix 168 * based on collection group found in the component context 169 * 170 * @param component - component instance for which the prefix is configured on 171 * @return String line binding path or empty string if path not found 172 */ 173 protected static String getLinePathPrefixValue(Component component) { 174 String linePath = ""; 175 176 CollectionGroup collectionGroup = (CollectionGroup) (component.getContext().get( 177 UifConstants.ContextVariableNames.COLLECTION_GROUP)); 178 if (collectionGroup == null) { 179 return linePath; 180 } 181 182 Object indexObj = component.getContext().get(UifConstants.ContextVariableNames.INDEX); 183 if (indexObj != null) { 184 int index = (Integer) indexObj; 185 boolean addLine = false; 186 Object addLineObj = component.getContext().get(UifConstants.ContextVariableNames.IS_ADD_LINE); 187 188 if (addLineObj != null) { 189 addLine = (Boolean) addLineObj; 190 } 191 192 if (addLine) { 193 linePath = collectionGroup.getAddLineBindingInfo().getBindingPath(); 194 } else { 195 linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]"; 196 } 197 } 198 199 return linePath; 200 } 201 202 /** 203 * Moves any nested property expressions to the parent object 204 * 205 * @param object - the object containing the expression 206 * @param propertyName - the property the expression is on 207 * @param expression - the expression to move 208 * @return 209 */ 210 protected static boolean moveNestedPropertyExpression(Object object, String propertyName, String expression) { 211 boolean moved = false; 212 213 // get the parent object for the property 214 String parentPropertyName = StringUtils.substringBeforeLast(propertyName, "."); 215 String propertyNameInParent = StringUtils.substringAfterLast(propertyName, "."); 216 217 Object parentObject = ObjectPropertyUtils.getPropertyValue(object, parentPropertyName); 218 if ((parentObject != null) && ObjectPropertyUtils.isReadableProperty(parentObject, 219 UifPropertyPaths.PROPERTY_EXPRESSIONS) && ((parentObject instanceof Component) 220 || (parentObject instanceof LayoutManager) 221 || (parentObject instanceof BindingInfo))) { 222 Map<String, String> propertyExpressions = ObjectPropertyUtils.getPropertyValue(parentObject, 223 UifPropertyPaths.PROPERTY_EXPRESSIONS); 224 if (propertyExpressions == null) { 225 propertyExpressions = new HashMap<String, String>(); 226 } 227 228 // add expression to map on parent object 229 propertyExpressions.put(propertyNameInParent, expression); 230 ObjectPropertyUtils.setPropertyValue(parentObject, UifPropertyPaths.PROPERTY_EXPRESSIONS, 231 propertyExpressions); 232 moved = true; 233 } 234 235 return moved; 236 } 237 238 /** 239 * Takes in an expression and a list to be filled in with names(property names) 240 * of controls found in the expression. This method returns a js expression which can 241 * be executed on the client to determine if the original exp was satisfied before 242 * interacting with the server - ie, this js expression is equivalent to the one passed in. 243 * 244 * There are limitations on the Spring expression language that can be used as this method. 245 * It is only used to parse expressions which are valid case statements for determining if 246 * some action/processing should be performed. ONLY Properties, comparison operators, booleans, 247 * strings, matches expression, and boolean logic are supported. Properties must 248 * be a valid property on the form, and should have a visible control within the view. 249 * 250 * Example valid exp: account.name == 'Account Name' 251 * 252 * @param exp 253 * @param controlNames 254 * @return 255 */ 256 public static String parseExpression(String exp, List<String> controlNames) { 257 // clean up expression to ease parsing 258 exp = exp.trim(); 259 if (exp.startsWith("@{")) { 260 exp = StringUtils.removeStart(exp, "@{"); 261 if (exp.endsWith("}")) { 262 exp = StringUtils.removeEnd(exp, "}"); 263 } 264 } 265 266 exp = StringUtils.replace(exp, "!=", " != "); 267 exp = StringUtils.replace(exp, "==", " == "); 268 exp = StringUtils.replace(exp, ">", " > "); 269 exp = StringUtils.replace(exp, "<", " < "); 270 exp = StringUtils.replace(exp, "<=", " <= "); 271 exp = StringUtils.replace(exp, ">=", " >= "); 272 273 String conditionJs = exp; 274 String stack = ""; 275 276 boolean expectingSingleQuote = false; 277 boolean ignoreNext = false; 278 for (int i = 0; i < exp.length(); i++) { 279 char c = exp.charAt(i); 280 if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) { 281 evaluateCurrentStack(stack.trim(), controlNames); 282 //reset stack 283 stack = ""; 284 continue; 285 } else if (!ignoreNext && c == '\'') { 286 stack = stack + c; 287 expectingSingleQuote = !expectingSingleQuote; 288 } else if (c == '\\') { 289 stack = stack + c; 290 ignoreNext = !ignoreNext; 291 } else { 292 stack = stack + c; 293 ignoreNext = false; 294 } 295 } 296 297 if (StringUtils.isNotEmpty(stack)) { 298 evaluateCurrentStack(stack.trim(), controlNames); 299 } 300 301 conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll( 302 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ") 303 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s", 304 " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll( 305 "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)"); 306 307 if (conditionJs.contains("matches")) { 308 conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null "); 309 conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/"); 310 conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)"); 311 } 312 313 for (String propertyName : controlNames) { 314 conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")"); 315 } 316 317 return conditionJs; 318 } 319 320 /** 321 * Used internally by parseExpression to evalute if the current stack is a property 322 * name (ie, will be a control on the form) 323 * 324 * @param stack 325 * @param controlNames 326 */ 327 public static void evaluateCurrentStack(String stack, List<String> controlNames) { 328 if (StringUtils.isNotBlank(stack)) { 329 if (!(stack.equals("==") 330 || stack.equals("!=") 331 || stack.equals(">") 332 || stack.equals("<") 333 || stack.equals(">=") 334 || stack.equals("<=") 335 || stack.equalsIgnoreCase("ne") 336 || stack.equalsIgnoreCase("eq") 337 || stack.equalsIgnoreCase("gt") 338 || stack.equalsIgnoreCase("lt") 339 || stack.equalsIgnoreCase("lte") 340 || stack.equalsIgnoreCase("gte") 341 || stack.equalsIgnoreCase("matches") 342 || stack.equalsIgnoreCase("null") 343 || stack.equalsIgnoreCase("false") 344 || stack.equalsIgnoreCase("true") 345 || stack.equalsIgnoreCase("and") 346 || stack.equalsIgnoreCase("or") 347 || stack.contains("#empty") 348 || stack.startsWith("'") 349 || stack.endsWith("'"))) { 350 351 boolean isNumber = false; 352 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) { 353 try { 354 Double.parseDouble(stack); 355 isNumber = true; 356 } catch (NumberFormatException e) { 357 isNumber = false; 358 } 359 } 360 361 if (!(isNumber)) { 362 if (!controlNames.contains(stack)) { 363 controlNames.add(stack); 364 } 365 } 366 } 367 } 368 } 369 370}