001 /* 002 * Copyright 2007 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 1.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/ecl1.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.kuali.rice.krad.uif.UifConstants; 020 import org.kuali.rice.krad.uif.UifPropertyPaths; 021 import org.kuali.rice.krad.uif.view.View; 022 import org.kuali.rice.krad.uif.component.BindingInfo; 023 import org.kuali.rice.krad.uif.component.Component; 024 import org.kuali.rice.krad.uif.layout.LayoutManager; 025 026 import java.util.HashMap; 027 import java.util.List; 028 import java.util.Map; 029 030 /** 031 * Utility class for UIF expressions 032 * 033 * @author Kuali Rice Team (rice.collab@kuali.org) 034 */ 035 public class ExpressionUtils { 036 037 /** 038 * @param view 039 * @param object 040 */ 041 public static void adjustPropertyExpressions(View view, Object object) { 042 if (object == null) { 043 return; 044 } 045 046 // get the map of property expressions to adjust 047 Map<String, String> propertyExpressions = new HashMap<String, String>(); 048 if (Component.class.isAssignableFrom(object.getClass())) { 049 propertyExpressions = ((Component) object).getPropertyExpressions(); 050 } else if (LayoutManager.class.isAssignableFrom(object.getClass())) { 051 propertyExpressions = ((LayoutManager) object).getPropertyExpressions(); 052 } else if (BindingInfo.class.isAssignableFrom(object.getClass())) { 053 propertyExpressions = ((BindingInfo) object).getPropertyExpressions(); 054 } 055 056 boolean defaultPathSet = StringUtils.isNotBlank(view.getDefaultBindingObjectPath()); 057 058 Map<String, String> adjustedPropertyExpressions = new HashMap<String, String>(); 059 for (Map.Entry<String, String> propertyExpression : propertyExpressions.entrySet()) { 060 String propertyName = propertyExpression.getKey(); 061 String expression = propertyExpression.getValue(); 062 063 // if property name is nested, need to move the expression to the parent object 064 if (StringUtils.contains(propertyName, ".")) { 065 boolean expressionMoved = moveNestedPropertyExpression(object, propertyName, expression); 066 067 // if expression moved, skip rest of control statement so it is not added to the adjusted map 068 if (expressionMoved) { 069 continue; 070 } 071 } 072 073 adjustedPropertyExpressions.put(propertyName, expression); 074 } 075 076 // update property expressions map on object 077 ObjectPropertyUtils.setPropertyValue(object, UifPropertyPaths.PROPERTY_EXPRESSIONS, 078 adjustedPropertyExpressions); 079 080 // TODO: In progress, adjusting paths in expressions 081 // if (defaultPathSet) { 082 // String adjustedExpression = ""; 083 // 084 // // check for expression placeholder wrapper or multiple expressions (template) 085 // if (StringUtils.contains(expression, "@{") && StringUtils.contains(expression, "}")) { 086 // String remainder = expression; 087 // 088 // while (StringUtils.isNotBlank(remainder) && StringUtils.contains(remainder, "@{") && StringUtils 089 // .contains(remainder, "}")) { 090 // String beforeLiteral = StringUtils.substringBefore(remainder, "@{"); 091 // String afterBeginDelimiter = StringUtils.substringAfter(remainder, "@{"); 092 // String nestedExpression = StringUtils.substringBefore(afterBeginDelimiter, "}"); 093 // remainder = StringUtils.substringAfter(afterBeginDelimiter, "}"); 094 // 095 // if (StringUtils.isNotBlank(beforeLiteral)) { 096 // adjustedExpression += beforeLiteral; 097 // } 098 // adjustedExpression += "@{"; 099 // 100 // if (StringUtils.isNotBlank(nestedExpression)) { 101 // String adjustedNestedExpression = processExpression(nestedExpression, 102 // view.getDefaultBindingObjectPath()); 103 // adjustedExpression += adjustedNestedExpression; 104 // } 105 // adjustedExpression += "}"; 106 // } 107 // 108 // // add last remainder if was a literal (did not contain expression placeholders) 109 // if (StringUtils.isNotBlank(remainder)) { 110 // adjustedExpression += remainder; 111 // } 112 // } else { 113 // // treat expression as one 114 // adjustedExpression = processExpression(expression, view.getDefaultBindingObjectPath()); 115 // } 116 // 117 // adjustedPropertyExpressions.put(propertyName, adjustedExpression); 118 // } else { 119 // adjustedPropertyExpressions.put(propertyName, expression); 120 // } 121 // } 122 // 123 124 } 125 126 protected static String processExpression(String expression, String pathPrefix) { 127 String processedExpression = ""; 128 129 Tokenizer tokenizer = new Tokenizer(expression); 130 tokenizer.process(); 131 132 Tokenizer.Token previousToken = null; 133 for (Tokenizer.Token token : tokenizer.getTokens()) { 134 if (token.isIdentifier()) { 135 // if an identifier, verify it is a model property name (must be at beginning of expression of 136 // come after a space) 137 String identifier = token.stringValue(); 138 if ((previousToken == null) || (previousToken.isIdentifier() && StringUtils.isBlank( 139 previousToken.stringValue()))) { 140 // append path prefix unless specified as form property 141 if (!StringUtils.startsWith(identifier, "form")) { 142 identifier = pathPrefix + "." + identifier; 143 } 144 } 145 processedExpression += identifier; 146 } else if (token.getKind().tokenChars.length != 0) { 147 processedExpression += new String(token.getKind().tokenChars); 148 } else { 149 processedExpression += token.stringValue(); 150 } 151 152 previousToken = token; 153 } 154 155 // remove special binding prefixes 156 processedExpression = StringUtils.replace(processedExpression, UifConstants.NO_BIND_ADJUST_PREFIX, ""); 157 158 return processedExpression; 159 } 160 161 protected static boolean moveNestedPropertyExpression(Object object, String propertyName, String expression) { 162 boolean moved = false; 163 164 // get the parent object for the property 165 String parentPropertyName = StringUtils.substringBeforeLast(propertyName, "."); 166 String propertyNameInParent = StringUtils.substringAfterLast(propertyName, "."); 167 168 Object parentObject = ObjectPropertyUtils.getPropertyValue(object, parentPropertyName); 169 if ((parentObject != null) && ObjectPropertyUtils.isReadableProperty(parentObject, 170 UifPropertyPaths.PROPERTY_EXPRESSIONS) && ((parentObject instanceof Component) 171 || (parentObject instanceof LayoutManager) 172 || (parentObject instanceof BindingInfo))) { 173 Map<String, String> propertyExpressions = ObjectPropertyUtils.getPropertyValue(parentObject, 174 UifPropertyPaths.PROPERTY_EXPRESSIONS); 175 if (propertyExpressions == null) { 176 propertyExpressions = new HashMap<String, String>(); 177 } 178 179 // add expression to map on parent object 180 propertyExpressions.put(propertyNameInParent, expression); 181 ObjectPropertyUtils.setPropertyValue(parentObject, UifPropertyPaths.PROPERTY_EXPRESSIONS, 182 propertyExpressions); 183 moved = true; 184 } 185 186 return moved; 187 } 188 189 /** 190 * Takes in an expression and a list to be filled in with names(property names) 191 * of controls found in the expression. This method returns a js expression which can 192 * be executed on the client to determine if the original exp was satisfied before 193 * interacting with the server - ie, this js expression is equivalent to the one passed in. 194 * 195 * There are limitations on the Spring expression language that can be used as this method. 196 * It is only used to parse expressions which are valid case statements for determining if 197 * some action/processing should be performed. ONLY Properties, comparison operators, booleans, 198 * strings, matches expression, and boolean logic are supported. Properties must 199 * be a valid property on the form, and should have a visible control within the view. 200 * 201 * Example valid exp: account.name == 'Account Name' 202 * 203 * @param exp 204 * @param controlNames 205 * @return 206 */ 207 public static String parseExpression(String exp, List<String> controlNames) { 208 // clean up expression to ease parsing 209 exp = exp.trim(); 210 if(exp.startsWith("@{")){ 211 exp = StringUtils.removeStart(exp, "@{"); 212 if(exp.endsWith("}")){ 213 exp = StringUtils.removeEnd(exp,"}"); 214 } 215 } 216 217 exp = StringUtils.replace(exp, "!=", " != "); 218 exp = StringUtils.replace(exp, "==", " == "); 219 exp = StringUtils.replace(exp, ">", " > "); 220 exp = StringUtils.replace(exp, "<", " < "); 221 exp = StringUtils.replace(exp, "<=", " <= "); 222 exp = StringUtils.replace(exp, ">=", " >= "); 223 224 225 String conditionJs = exp; 226 String stack = ""; 227 228 boolean expectingSingleQuote = false; 229 boolean ignoreNext = false; 230 for (int i = 0; i < exp.length(); i++) { 231 char c = exp.charAt(i); 232 if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) { 233 evaluateCurrentStack(stack.trim(), controlNames); 234 //reset stack 235 stack = ""; 236 continue; 237 } else if (!ignoreNext && c == '\'') { 238 stack = stack + c; 239 expectingSingleQuote = !expectingSingleQuote; 240 } else if (c == '\\') { 241 stack = stack + c; 242 ignoreNext = !ignoreNext; 243 } else { 244 stack = stack + c; 245 ignoreNext = false; 246 } 247 } 248 249 if(StringUtils.isNotEmpty(stack)){ 250 evaluateCurrentStack(stack.trim(), controlNames); 251 } 252 253 conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll( 254 "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ") 255 .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s", 256 " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll( 257 "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)"); 258 259 if (conditionJs.contains("matches")) { 260 conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null "); 261 conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/"); 262 conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)"); 263 } 264 265 for (String propertyName : controlNames) { 266 conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")"); 267 } 268 269 return conditionJs; 270 } 271 272 /** 273 * Used internally by parseExpression to evalute if the current stack is a property 274 * name (ie, will be a control on the form) 275 * 276 * @param stack 277 * @param controlNames 278 */ 279 public static void evaluateCurrentStack(String stack, List<String> controlNames) { 280 if (StringUtils.isNotBlank(stack)) { 281 if (!(stack.equals("==") 282 || stack.equals("!=") 283 || stack.equals(">") 284 || stack.equals("<") 285 || stack.equals(">=") 286 || stack.equals("<=") 287 || stack.equalsIgnoreCase("ne") 288 || stack.equalsIgnoreCase("eq") 289 || stack.equalsIgnoreCase("gt") 290 || stack.equalsIgnoreCase("lt") 291 || stack.equalsIgnoreCase("lte") 292 || stack.equalsIgnoreCase("gte") 293 || stack.equalsIgnoreCase("matches") 294 || stack.equalsIgnoreCase("null") 295 || stack.equalsIgnoreCase("false") 296 || stack.equalsIgnoreCase("true") 297 || stack.equalsIgnoreCase("and") 298 || stack.equalsIgnoreCase("or") 299 || stack.contains("#empty") 300 || stack.startsWith("'") 301 || stack.endsWith("'"))) { 302 303 boolean isNumber = false; 304 if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) { 305 try { 306 Double.parseDouble(stack); 307 isNumber = true; 308 } catch (NumberFormatException e) { 309 isNumber = false; 310 } 311 } 312 313 if (!(isNumber)) { 314 if (!controlNames.contains(stack)) { 315 controlNames.add(stack); 316 } 317 } 318 } 319 } 320 } 321 322 }