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 }