001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.krad.uif.util;
017
018import org.apache.commons.lang.StringUtils;
019import org.kuali.rice.krad.uif.UifConstants;
020import org.kuali.rice.krad.uif.UifPropertyPaths;
021import org.kuali.rice.krad.uif.container.CollectionGroup;
022import org.kuali.rice.krad.uif.field.DataField;
023import org.kuali.rice.krad.uif.view.View;
024import org.kuali.rice.krad.uif.component.BindingInfo;
025import org.kuali.rice.krad.uif.component.Component;
026import org.kuali.rice.krad.uif.layout.LayoutManager;
027
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031
032/**
033 * Utility class for UIF expressions
034 *
035 * @author Kuali Rice Team (rice.collab@kuali.org)
036 */
037public class ExpressionUtils {
038
039    /**
040     * Adjusts the property expressions for a given object. Any nested properties are moved to the parent
041     * object. Binding adjust prefixes are replaced with the correct values.
042     *
043     * <p>
044     * The org.kuali.rice.krad.uif.UifConstants#NO_BIND_ADJUST_PREFIX prefix will be removed
045     * as this is a placeholder indicating that the property is directly on the form.
046     * The org.kuali.rice.krad.uif.UifConstants#FIELD_PATH_BIND_ADJUST_PREFIX prefix will be replaced by
047     * the object's field path - this is only applicable to DataFields. The
048     * org.kuali.rice.krad.uif.UifConstants#DEFAULT_PATH_BIND_ADJUST_PREFIX prefix will be replaced
049     * by the view's default path if it is set.
050     * </p>
051     *
052     * @param view - the parent view of the object
053     * @param object - Object to adjust property expressions on
054     */
055    public static void adjustPropertyExpressions(View view, Object object) {
056        if (object == null) {
057            return;
058        }
059
060        // get the map of property expressions to adjust
061        Map<String, String> propertyExpressions = new HashMap<String, String>();
062        if (Component.class.isAssignableFrom(object.getClass())) {
063            propertyExpressions = ((Component) object).getPropertyExpressions();
064        } else if (LayoutManager.class.isAssignableFrom(object.getClass())) {
065            propertyExpressions = ((LayoutManager) object).getPropertyExpressions();
066        } else if (BindingInfo.class.isAssignableFrom(object.getClass())) {
067            propertyExpressions = ((BindingInfo) object).getPropertyExpressions();
068        }
069
070        Map<String, String> adjustedPropertyExpressions = new HashMap<String, String>();
071        for (Map.Entry<String, String> propertyExpression : propertyExpressions.entrySet()) {
072            String propertyName = propertyExpression.getKey();
073            String expression = propertyExpression.getValue();
074
075            // if property name is nested, need to move the expression to the parent object
076            if (StringUtils.contains(propertyName, ".")) {
077                boolean expressionMoved = moveNestedPropertyExpression(object, propertyName, expression);
078
079                // if expression moved, skip rest of control statement so it is not added to the adjusted map
080                if (expressionMoved) {
081                    continue;
082                }
083            }
084
085            // replace the binding prefixes
086            String adjustedExpression = replaceBindingPrefixes(view, object, expression);
087
088            adjustedPropertyExpressions.put(propertyName, adjustedExpression);
089        }
090
091        // update property expressions map on object
092        ObjectPropertyUtils.setPropertyValue(object, UifPropertyPaths.PROPERTY_EXPRESSIONS,
093                adjustedPropertyExpressions);
094    }
095
096    /**
097     * Adjusts the property expressions for a given object
098     *
099     * <p>
100     * The org.kuali.rice.krad.uif.UifConstants#NO_BIND_ADJUST_PREFIX prefix will be removed
101     * as this is a placeholder indicating that the property is directly on the form.
102     * The org.kuali.rice.krad.uif.UifConstants#FIELD_PATH_BIND_ADJUST_PREFIX prefix will be replaced by
103     * the object's field path - this is only applicable to DataFields. The
104     * org.kuali.rice.krad.uif.UifConstants#DEFAULT_PATH_BIND_ADJUST_PREFIX prefix will be replaced
105     * by the view's default path if it is set.
106     * </p>
107     *
108     * @param view - the parent view of the object
109     * @param object - Object to adjust property expressions on
110     * @param expression - The expression to adjust
111     * @return the adjusted expression String
112     */
113    public static String replaceBindingPrefixes(View view, Object object, String expression) {
114        String adjustedExpression = StringUtils.replace(expression, UifConstants.NO_BIND_ADJUST_PREFIX, "");
115
116        // replace the field path prefix for DataFields
117        if (object instanceof DataField) {
118
119            // Get the binding path from the object
120            BindingInfo bindingInfo = ((DataField) object).getBindingInfo();
121            String fieldPath = bindingInfo.getBindingPath();
122
123            // Remove the property name from the binding path
124            fieldPath = StringUtils.removeEnd(fieldPath, "." + bindingInfo.getBindingName());
125            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
126                    fieldPath + ".");
127        } else {
128            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.FIELD_PATH_BIND_ADJUST_PREFIX,
129                    "");
130        }
131
132        // replace the default path prefix if there is one set on the view
133        if (StringUtils.isNotBlank(view.getDefaultBindingObjectPath())) {
134            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
135                    view.getDefaultBindingObjectPath() + ".");
136
137        } else {
138            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.DEFAULT_PATH_BIND_ADJUST_PREFIX,
139                    "");
140        }
141
142        // replace line path binding prefix with the actual line path
143        if (adjustedExpression.contains(UifConstants.LINE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
144            String linePath = getLinePathPrefixValue((Component) object);
145
146            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.LINE_PATH_BIND_ADJUST_PREFIX,
147                    linePath + ".");
148        }
149
150        // replace node path binding prefix with the actual node path
151        if (adjustedExpression.contains(UifConstants.NODE_PATH_BIND_ADJUST_PREFIX) && (object instanceof Component)) {
152            String nodePath = "";
153
154            Map<String, Object> context = ((Component) object).getContext();
155            if (context.containsKey(UifConstants.ContextVariableNames.NODE_PATH)) {
156                nodePath = (String) context.get(UifConstants.ContextVariableNames.NODE_PATH);
157            }
158
159            adjustedExpression = StringUtils.replace(adjustedExpression, UifConstants.NODE_PATH_BIND_ADJUST_PREFIX,
160                    nodePath + ".");
161        }
162
163        return adjustedExpression;
164    }
165
166    /**
167     * Determines the value for the org.kuali.rice.krad.uif.UifConstants#LINE_PATH_BIND_ADJUST_PREFIX binding prefix
168     * based on collection group found in the component context
169     *
170     * @param component - component instance for which the prefix is configured on
171     * @return String line binding path or empty string if path not found
172     */
173    protected static String getLinePathPrefixValue(Component component) {
174        String linePath = "";
175
176        CollectionGroup collectionGroup = (CollectionGroup) (component.getContext().get(
177                UifConstants.ContextVariableNames.COLLECTION_GROUP));
178        if (collectionGroup == null) {
179            return linePath;
180        }
181
182        Object indexObj = component.getContext().get(UifConstants.ContextVariableNames.INDEX);
183        if (indexObj != null) {
184            int index = (Integer) indexObj;
185            boolean addLine = false;
186            Object addLineObj = component.getContext().get(UifConstants.ContextVariableNames.IS_ADD_LINE);
187
188            if (addLineObj != null) {
189                addLine = (Boolean) addLineObj;
190            }
191
192            if (addLine) {
193                linePath = collectionGroup.getAddLineBindingInfo().getBindingPath();
194            } else {
195                linePath = collectionGroup.getBindingInfo().getBindingPath() + "[" + index + "]";
196            }
197        }
198
199        return linePath;
200    }
201
202    /**
203     * Moves any nested property expressions to the parent object
204     *
205     * @param object - the object containing the expression
206     * @param propertyName - the property the expression is on
207     * @param expression - the expression to move
208     * @return
209     */
210    protected static boolean moveNestedPropertyExpression(Object object, String propertyName, String expression) {
211        boolean moved = false;
212
213        // get the parent object for the property
214        String parentPropertyName = StringUtils.substringBeforeLast(propertyName, ".");
215        String propertyNameInParent = StringUtils.substringAfterLast(propertyName, ".");
216
217        Object parentObject = ObjectPropertyUtils.getPropertyValue(object, parentPropertyName);
218        if ((parentObject != null) && ObjectPropertyUtils.isReadableProperty(parentObject,
219                UifPropertyPaths.PROPERTY_EXPRESSIONS) && ((parentObject instanceof Component)
220                || (parentObject instanceof LayoutManager)
221                || (parentObject instanceof BindingInfo))) {
222            Map<String, String> propertyExpressions = ObjectPropertyUtils.getPropertyValue(parentObject,
223                    UifPropertyPaths.PROPERTY_EXPRESSIONS);
224            if (propertyExpressions == null) {
225                propertyExpressions = new HashMap<String, String>();
226            }
227
228            // add expression to map on parent object
229            propertyExpressions.put(propertyNameInParent, expression);
230            ObjectPropertyUtils.setPropertyValue(parentObject, UifPropertyPaths.PROPERTY_EXPRESSIONS,
231                    propertyExpressions);
232            moved = true;
233        }
234
235        return moved;
236    }
237
238    /**
239     * Takes in an expression and a list to be filled in with names(property names)
240     * of controls found in the expression. This method returns a js expression which can
241     * be executed on the client to determine if the original exp was satisfied before
242     * interacting with the server - ie, this js expression is equivalent to the one passed in.
243     *
244     * There are limitations on the Spring expression language that can be used as this method.
245     * It is only used to parse expressions which are valid case statements for determining if
246     * some action/processing should be performed.  ONLY Properties, comparison operators, booleans,
247     * strings, matches expression, and boolean logic are supported.  Properties must
248     * be a valid property on the form, and should have a visible control within the view.
249     *
250     * Example valid exp: account.name == 'Account Name'
251     *
252     * @param exp
253     * @param controlNames
254     * @return
255     */
256    public static String parseExpression(String exp, List<String> controlNames) {
257        // clean up expression to ease parsing
258        exp = exp.trim();
259        if (exp.startsWith("@{")) {
260            exp = StringUtils.removeStart(exp, "@{");
261            if (exp.endsWith("}")) {
262                exp = StringUtils.removeEnd(exp, "}");
263            }
264        }
265
266        exp = StringUtils.replace(exp, "!=", " != ");
267        exp = StringUtils.replace(exp, "==", " == ");
268        exp = StringUtils.replace(exp, ">", " > ");
269        exp = StringUtils.replace(exp, "<", " < ");
270        exp = StringUtils.replace(exp, "<=", " <= ");
271        exp = StringUtils.replace(exp, ">=", " >= ");
272
273        String conditionJs = exp;
274        String stack = "";
275
276        boolean expectingSingleQuote = false;
277        boolean ignoreNext = false;
278        for (int i = 0; i < exp.length(); i++) {
279            char c = exp.charAt(i);
280            if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) {
281                evaluateCurrentStack(stack.trim(), controlNames);
282                //reset stack
283                stack = "";
284                continue;
285            } else if (!ignoreNext && c == '\'') {
286                stack = stack + c;
287                expectingSingleQuote = !expectingSingleQuote;
288            } else if (c == '\\') {
289                stack = stack + c;
290                ignoreNext = !ignoreNext;
291            } else {
292                stack = stack + c;
293                ignoreNext = false;
294            }
295        }
296
297        if (StringUtils.isNotEmpty(stack)) {
298            evaluateCurrentStack(stack.trim(), controlNames);
299        }
300
301        conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ").replaceAll(
302                "\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ").replaceAll("\\s(?i:lte)\\s", " <= ")
303                .replaceAll("\\s(?i:gte)\\s", " >= ").replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s",
304                        " || ").replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ").replaceAll(
305                        "\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)");
306
307        if (conditionJs.contains("matches")) {
308            conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null ");
309            conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/");
310            conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)");
311        }
312
313        for (String propertyName : controlNames) {
314            conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")");
315        }
316
317        return conditionJs;
318    }
319
320    /**
321     * Used internally by parseExpression to evalute if the current stack is a property
322     * name (ie, will be a control on the form)
323     *
324     * @param stack
325     * @param controlNames
326     */
327    public static void evaluateCurrentStack(String stack, List<String> controlNames) {
328        if (StringUtils.isNotBlank(stack)) {
329            if (!(stack.equals("==")
330                    || stack.equals("!=")
331                    || stack.equals(">")
332                    || stack.equals("<")
333                    || stack.equals(">=")
334                    || stack.equals("<=")
335                    || stack.equalsIgnoreCase("ne")
336                    || stack.equalsIgnoreCase("eq")
337                    || stack.equalsIgnoreCase("gt")
338                    || stack.equalsIgnoreCase("lt")
339                    || stack.equalsIgnoreCase("lte")
340                    || stack.equalsIgnoreCase("gte")
341                    || stack.equalsIgnoreCase("matches")
342                    || stack.equalsIgnoreCase("null")
343                    || stack.equalsIgnoreCase("false")
344                    || stack.equalsIgnoreCase("true")
345                    || stack.equalsIgnoreCase("and")
346                    || stack.equalsIgnoreCase("or")
347                    || stack.contains("#empty")
348                    || stack.startsWith("'")
349                    || stack.endsWith("'"))) {
350
351                boolean isNumber = false;
352                if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) {
353                    try {
354                        Double.parseDouble(stack);
355                        isNumber = true;
356                    } catch (NumberFormatException e) {
357                        isNumber = false;
358                    }
359                }
360
361                if (!(isNumber)) {
362                    if (!controlNames.contains(stack)) {
363                        controlNames.add(stack);
364                    }
365                }
366            }
367        }
368    }
369
370}