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 }