azkaban-developers
Changes
src/main/java/azkaban/utils/PropsUtils.java 68(+66 -2)
Details
src/main/java/azkaban/utils/PropsUtils.java 68(+66 -2)
diff --git a/src/main/java/azkaban/utils/PropsUtils.java b/src/main/java/azkaban/utils/PropsUtils.java
index 37cd003..2f826a3 100644
--- a/src/main/java/azkaban/utils/PropsUtils.java
+++ b/src/main/java/azkaban/utils/PropsUtils.java
@@ -31,6 +31,10 @@ import java.util.regex.Pattern;
import azkaban.executor.ExecutableFlowBase;
import azkaban.flow.CommonJobProperties;
+import org.apache.commons.jexl2.Expression;
+import org.apache.commons.jexl2.JexlEngine;
+import org.apache.commons.jexl2.JexlException;
+import org.apache.commons.jexl2.MapContext;
import org.apache.commons.lang.StringUtils;
import org.joda.time.DateTime;
@@ -144,7 +148,7 @@ public class PropsUtils {
return false;
}
- private static final Pattern VARIABLE_PATTERN = Pattern.compile("\\$\\{([a-zA-Z_.0-9]+)\\}");
+ private static final Pattern VARIABLE_REPLACEMENT_PATTERN = Pattern.compile("\\$\\{([a-zA-Z_.0-9]+)\\}");
public static Props resolveProps(Props props) {
if (props == null) return null;
@@ -162,6 +166,12 @@ public class PropsUtils {
resolvedProps.put(key, replacedValue);
}
+ for (String key : resolvedProps.getKeySet()) {
+ String value = resolvedProps.get(key);
+ String expressedValue = resolveVariableExpression(value);
+ resolvedProps.put(key, expressedValue);
+ }
+
return resolvedProps;
};
@@ -169,7 +179,7 @@ public class PropsUtils {
StringBuffer buffer = new StringBuffer();
int startIndex = 0;
- Matcher matcher = VARIABLE_PATTERN.matcher(value);
+ Matcher matcher = VARIABLE_REPLACEMENT_PATTERN.matcher(value);
while (matcher.find(startIndex)) {
if (startIndex < matcher.start()) {
// Copy everything up front to the buffer
@@ -208,6 +218,60 @@ public class PropsUtils {
return buffer.toString();
}
+ private static String resolveVariableExpression(String value) {
+ JexlEngine jexl = new JexlEngine();
+ return resolveVariableExpression(value, value.length(), jexl);
+ }
+
+ /**
+ * Function that looks for expressions to parse.
+ * It parses backwards to capture embedded expressions
+ *
+ * @param value
+ * @param last
+ * @param jexl
+ * @return
+ */
+ private static String resolveVariableExpression(String value, int last, JexlEngine jexl) {
+ int lastIndex = value.lastIndexOf("$(", last);
+ if (lastIndex == -1) {
+ return value;
+ }
+
+ // Want to check that everything is well formed, and that
+ // we properly capture $( ...(...)...).
+ int bracketCount = 0;
+ int nextClosed = lastIndex + 2;
+ for (; nextClosed < value.length(); ++nextClosed) {
+ if (value.charAt(nextClosed) == '(') {
+ bracketCount++;
+ }
+ else if (value.charAt(nextClosed) == ')') {
+ bracketCount--;
+ if (bracketCount == -1) {
+ break;
+ }
+ }
+ }
+
+ if (nextClosed == value.length()) {
+ throw new IllegalArgumentException("Expression " + value + " not well formed.");
+ }
+
+ String innerExpression = value.substring(lastIndex + 2, nextClosed);
+ Object result = null;
+ try {
+ Expression e = jexl.createExpression(innerExpression);
+ result = e.evaluate(new MapContext());
+ }
+ catch (JexlException e) {
+ throw new IllegalArgumentException("Expression " + value + " not well formed. " + e.getMessage(), e);
+ }
+
+ String newValue = value.substring(0, lastIndex) + result.toString() + value.substring(nextClosed + 1);
+ return resolveVariableExpression(newValue, lastIndex, jexl);
+ }
+
public static Props addCommonFlowProperties(Props parentProps, final ExecutableFlowBase flow) {
Props props = new Props(parentProps);
diff --git a/unit/java/azkaban/test/utils/PropsUtilsTest.java b/unit/java/azkaban/test/utils/PropsUtilsTest.java
index 09edb89..6b34b48 100644
--- a/unit/java/azkaban/test/utils/PropsUtilsTest.java
+++ b/unit/java/azkaban/test/utils/PropsUtilsTest.java
@@ -51,6 +51,64 @@ public class PropsUtilsTest {
}
@Test
+ public void testExpressionResolution() throws IOException {
+ Props props = Props.of(
+ "normkey", "normal",
+ "num1", "1",
+ "num2", "2",
+ "num3", "3",
+ "variablereplaced", "${num1}",
+ "expression1", "$(1+10)",
+ "expression2", "$(1+10)*2",
+ "expression3", "$($(${num1} + ${num3})*10)",
+ "expression4", "$(${num1} + ${expression3})",
+ "expression5", "$($($(2+3)) + 3) + $(${expression3} + 1)",
+ "expression6", "$(1 + ${normkey})",
+ "expression7", "$(\"${normkey}\" + 1)",
+ "expression8", "${expression1}",
+ "expression9", "$((2+3) + 3)"
+ );
+
+ Props resolved = PropsUtils.resolveProps(props);
+ Assert.assertEquals("normal", resolved.get("normkey"));
+ Assert.assertEquals("1", resolved.get("num1"));
+ Assert.assertEquals("2", resolved.get("num2"));
+ Assert.assertEquals("3", resolved.get("num3"));
+ Assert.assertEquals("1", resolved.get("variablereplaced"));
+ Assert.assertEquals("11", resolved.get("expression1"));
+ Assert.assertEquals("11*2", resolved.get("expression2"));
+ Assert.assertEquals("40", resolved.get("expression3"));
+ Assert.assertEquals("41", resolved.get("expression4"));
+ Assert.assertEquals("8 + 41", resolved.get("expression5"));
+ Assert.assertEquals("1", resolved.get("expression6"));
+ Assert.assertEquals("normal1", resolved.get("expression7"));
+ Assert.assertEquals("11", resolved.get("expression8"));
+ Assert.assertEquals("8", resolved.get("expression9"));
+ }
+
+ @Test
+ public void testMalformedExpressionProps() throws IOException {
+ // unclosed
+ Props props = Props.of("key", "$(1+2");
+ failIfNotException(props);
+
+ props = Props.of("key", "$((1+2)");
+ failIfNotException(props);
+
+ // bad variable replacement
+ props = Props.of("key", "$(${dontexist}+2)");
+ failIfNotException(props);
+
+ // bad expression
+ props = Props.of("key", "$(2 +)");
+ failIfNotException(props);
+
+ // bad expression
+ props = Props.of("key", "$(2 + #hello)");
+ failIfNotException(props);
+ }
+
+ @Test
public void testCyclesResolveProps() throws IOException {
Props propsGrandParent = new Props();
Props propsParent = new Props(propsGrandParent);