Java tutorial
/** * Copyright 2005-2014 The Kuali Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ecl2.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.kuali.rice.krad.uif.util; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.kuali.rice.core.api.exception.RiceRuntimeException; import org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Utility class for UIF expressions * * @author Kuali Rice Team (rice.collab@kuali.org) */ public class ExpressionUtils { private static final Log LOG = LogFactory.getLog(ExpressionUtils.class); /** * Pulls expressions within the expressionConfigurable's expression graph and moves them to the property * expressions * map for the expressionConfigurable or a nested expressionConfigurable (for the case of nested expression * property * names) * * <p> * Expressions that are configured on properties and pulled out by the {@link org.kuali.rice.krad.datadictionary.uif.UifBeanFactoryPostProcessor} * and put in the {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getExpressionGraph()} for the * bean * that is * at root (non nested) level. Before evaluating the expressions, they need to be moved to the * {@link org.kuali.rice.krad.datadictionary.uif.UifDictionaryBean#getPropertyExpressions()} map for the * expressionConfigurable that * property * is on. * </p> * * @param expressionConfigurable expressionConfigurable instance to process expressions for */ public static void populatePropertyExpressionsFromGraph(UifDictionaryBean expressionConfigurable) { if (expressionConfigurable == null || expressionConfigurable.getExpressionGraph() == null) { return; } // will hold graphs to populate the refreshExpressionGraph property on each expressionConfigurable // key is the path to the expressionConfigurable and value is the map of nested property names to expressions Map<String, Map<String, String>> refreshExpressionGraphs = new HashMap<String, Map<String, String>>(); Map<String, String> expressionGraph = expressionConfigurable.getExpressionGraph(); for (Map.Entry<String, String> expressionEntry : expressionGraph.entrySet()) { String propertyName = expressionEntry.getKey(); String expression = expressionEntry.getValue(); // by default assume expression belongs with passed in expressionConfigurable UifDictionaryBean configurableWithExpression = expressionConfigurable; // if property name is nested, we need to move the expression to the last expressionConfigurable String adjustedPropertyName = propertyName; if (StringUtils.contains(propertyName, ".")) { String configurablePath = StringUtils.substringBeforeLast(propertyName, "."); adjustedPropertyName = StringUtils.substringAfterLast(propertyName, "."); Object nestedObject = ObjectPropertyUtils.getPropertyValue(expressionConfigurable, configurablePath); // skip missing expression object for components skipping their lifecycle because objects // in these components may be missing (and are expected to be missing) if (nestedObject == null && expressionConfigurable instanceof LifecycleElement && ((LifecycleElement) expressionConfigurable).skipLifecycle()) { continue; } if ((nestedObject == null) || !(nestedObject instanceof UifDictionaryBean)) { throw new RiceRuntimeException( "Object for which expression is configured on is null or does not " + "implement UifDictionaryBean: '" + configurablePath + "' on class " + expressionConfigurable.getClass().getName() + " while evaluating " + "expression for " + propertyName); } // use nested object as the expressionConfigurable which will get the property expression configurableWithExpression = (UifDictionaryBean) nestedObject; } configurableWithExpression.getPropertyExpressions().put(adjustedPropertyName, expression); } } /** * Takes in an expression and a list to be filled in with names(property names) * of controls found in the expression. This method returns a js expression which can * be executed on the client to determine if the original exp was satisfied before * interacting with the server - ie, this js expression is equivalent to the one passed in. * * There are limitations on the Spring expression language that can be used as this method. * It is only used to parse expressions which are valid case statements for determining if * some action/processing should be performed. ONLY Properties, comparison operators, booleans, * strings, matches expression, and boolean logic are supported. Properties must * be a valid property on the form, and should have a visible control within the view. * * Example valid exp: account.name == 'Account Name' * * @param exp * @param controlNames * @return parsed expression, expressed as JS for client side evaluation */ public static String parseExpression(String exp, List<String> controlNames) { // clean up expression to ease parsing exp = exp.trim(); if (exp.startsWith("@{")) { exp = StringUtils.removeStart(exp, "@{"); if (exp.endsWith("}")) { exp = StringUtils.removeEnd(exp, "}"); } } exp = StringUtils.replace(exp, "!=", " != "); exp = StringUtils.replace(exp, "==", " == "); exp = StringUtils.replace(exp, ">", " > "); exp = StringUtils.replace(exp, "<", " < "); exp = StringUtils.replace(exp, "<=", " <= "); exp = StringUtils.replace(exp, ">=", " >= "); String conditionJs = exp; String stack = ""; boolean expectingSingleQuote = false; boolean ignoreNext = false; for (int i = 0; i < exp.length(); i++) { char c = exp.charAt(i); if (!expectingSingleQuote && !ignoreNext && (c == '(' || c == ' ' || c == ')')) { evaluateCurrentStack(stack.trim(), controlNames); //reset stack stack = ""; continue; } else if (!ignoreNext && c == '\'') { stack = stack + c; expectingSingleQuote = !expectingSingleQuote; } else if (c == '\\') { stack = stack + c; ignoreNext = !ignoreNext; } else { stack = stack + c; ignoreNext = false; } } if (StringUtils.isNotEmpty(stack)) { evaluateCurrentStack(stack.trim(), controlNames); } conditionJs = conditionJs.replaceAll("\\s(?i:ne)\\s", " != ").replaceAll("\\s(?i:eq)\\s", " == ") .replaceAll("\\s(?i:gt)\\s", " > ").replaceAll("\\s(?i:lt)\\s", " < ") .replaceAll("\\s(?i:lte)\\s", " <= ").replaceAll("\\s(?i:gte)\\s", " >= ") .replaceAll("\\s(?i:and)\\s", " && ").replaceAll("\\s(?i:or)\\s", " || ") .replaceAll("\\s(?i:not)\\s", " != ").replaceAll("\\s(?i:null)\\s?", " '' ") .replaceAll("\\s?(?i:#empty)\\((.*?)\\)", "isValueEmpty($1)") .replaceAll("\\s?(?i:#listContains)\\((.*?)\\)", "listContains($1)") .replaceAll("\\s?(?i:#emptyList)\\((.*?)\\)", "emptyList($1)"); if (conditionJs.contains("matches")) { conditionJs = conditionJs.replaceAll("\\s+(?i:matches)\\s+'.*'", ".match(/" + "$0" + "/) != null "); conditionJs = conditionJs.replaceAll("\\(/\\s+(?i:matches)\\s+'", "(/"); conditionJs = conditionJs.replaceAll("'\\s*/\\)", "/)"); } List<String> removeControlNames = new ArrayList<String>(); List<String> addControlNames = new ArrayList<String>(); //convert property names to use coerceValue function and convert arrays to js arrays for (String propertyName : controlNames) { //array definitions are caught in controlNames because of the nature of the parse - convert them and remove if (propertyName.trim().startsWith("{") && propertyName.trim().endsWith("}")) { String array = propertyName.trim().replace('{', '['); array = array.replace('}', ']'); conditionJs = conditionJs.replace(propertyName, array); removeControlNames.add(propertyName); continue; } //handle not if (propertyName.startsWith("!")) { String actualPropertyName = StringUtils.removeStart(propertyName, "!"); conditionJs = conditionJs.replace(propertyName, "!coerceValue(\"" + actualPropertyName + "\")"); removeControlNames.add(propertyName); addControlNames.add(actualPropertyName); } else { conditionJs = conditionJs.replace(propertyName, "coerceValue(\"" + propertyName + "\")"); } } controlNames.removeAll(removeControlNames); controlNames.addAll(addControlNames); return conditionJs; } /** * Used internally by parseExpression to evalute if the current stack is a property * name (ie, will be a control on the form) * * @param stack * @param controlNames */ public static void evaluateCurrentStack(String stack, List<String> controlNames) { if (StringUtils.isNotBlank(stack)) { if (!(stack.equals("==") || stack.equals("!=") || stack.equals(">") || stack.equals("<") || stack.equals(">=") || stack.equals("<=") || stack.equalsIgnoreCase("ne") || stack.equalsIgnoreCase("eq") || stack.equalsIgnoreCase("gt") || stack.equalsIgnoreCase("lt") || stack.equalsIgnoreCase("lte") || stack.equalsIgnoreCase("gte") || stack.equalsIgnoreCase("matches") || stack.equalsIgnoreCase("null") || stack.equalsIgnoreCase("false") || stack.equalsIgnoreCase("true") || stack.equalsIgnoreCase("and") || stack.equalsIgnoreCase("or") || stack.contains("#empty") || stack.equals("!") || stack.contains("#emptyList") || stack.contains("#listContains") || stack.startsWith("'") || stack.endsWith("'"))) { boolean isNumber = false; if ((StringUtils.isNumeric(stack.substring(0, 1)) || stack.substring(0, 1).equals("-"))) { try { Double.parseDouble(stack); isNumber = true; } catch (NumberFormatException e) { isNumber = false; } } if (!(isNumber)) { //correct argument of a custom function ending in comma if (StringUtils.endsWith(stack, ",")) { stack = StringUtils.removeEnd(stack, ",").trim(); } if (!controlNames.contains(stack)) { controlNames.add(stack); } } } } } }