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    }