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    }