azkaban-memoizeit

Details

diff --git a/src/java/azkaban/utils/PropsUtils.java b/src/java/azkaban/utils/PropsUtils.java
index e3c9c2d..e0dac83 100644
--- a/src/java/azkaban/utils/PropsUtils.java
+++ b/src/java/azkaban/utils/PropsUtils.java
@@ -20,6 +20,7 @@ import java.io.File;
 import java.io.IOException;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -146,45 +147,70 @@ public class PropsUtils {
 			.compile("\\$\\{([a-zA-Z_.0-9]+)\\}");
 
 	public static Props resolveProps(Props props) {
-		if(props == null) return null;
-		
+		if (props == null) {
+			return null;
+		}
+
 		Props resolvedProps = new Props();
 
+		LinkedHashSet<String> visitedVariables = new LinkedHashSet<String>();
 		for (String key : props.getKeySet()) {
-			StringBuffer replaced = new StringBuffer();
 			String value = props.get(key);
-			Matcher matcher = VARIABLE_PATTERN.matcher(value);
-			while (matcher.find()) {
-				String variableName = matcher.group(1);
 
-				if (variableName.equals(key)) {
-					throw new IllegalArgumentException(
-							String.format(
-									"Circular property definition starting from property[%s]",
-									key));
-				}
+			visitedVariables.add(key);
+			String replacedValue = resolveVariableReplacement(value, props,
+					visitedVariables);
+			visitedVariables.clear();
 
-				String replacement = props.get(variableName);
-				if (replacement == null)
-					throw new UndefinedPropertyException(
-							"Could not find variable substitution for variable '"
-									+ variableName + "' in key '" + key + "'.");
+			resolvedProps.put(key, replacedValue);
+		}
+
+		return resolvedProps;
+	};
 
-				replacement = replacement.replaceAll("\\\\", "\\\\\\\\");
-				replacement = replacement.replaceAll("\\$", "\\\\\\$");
+	private static String resolveVariableReplacement(String value, Props props, LinkedHashSet<String> visitedVariables) {
+		StringBuffer buffer = new StringBuffer();
+		int startIndex = 0;
 
-				matcher.appendReplacement(replaced, replacement);
-				matcher.appendTail(replaced);
+		Matcher matcher = VARIABLE_PATTERN.matcher(value);
+		while (matcher.find(startIndex)) {
+			if (startIndex < matcher.start()) {
+				// Copy everything up front to the buffer
+				buffer.append(value.substring(startIndex, matcher.start()));
+			}
+
+			String subVariable = matcher.group(1);
+			// Detected a cycle
+			if (visitedVariables.contains(subVariable)) {
+				throw new IllegalArgumentException(String.format(
+						"Circular variable substitution found: [%s] -> [%s]",
+						StringUtils.join(visitedVariables, "->"),
+						subVariable));
+			} else {
+				// Add substitute variable and recurse.
+				String replacement = props.get(subVariable);
+				visitedVariables.add(subVariable);
+
+				if (replacement == null) {
+					throw new UndefinedPropertyException(
+							String.format(
+									"Could not find variable substitution for variable(s) [%s]",
+									StringUtils.join(visitedVariables, "->")));
+				}
 
-				value = replaced.toString();
-				replaced = new StringBuffer();
-				matcher = VARIABLE_PATTERN.matcher(value);
+				buffer.append(resolveVariableReplacement(replacement, props,
+						visitedVariables));
+				visitedVariables.remove(subVariable);
 			}
-			matcher.appendTail(replaced);
-			resolvedProps.put(key, replaced.toString());
+
+			startIndex = matcher.end();
 		}
 
-		return resolvedProps;
+		if (startIndex < value.length()) {
+			buffer.append(value.substring(startIndex));
+		}
+
+		return buffer.toString();
 	}
 
 	public static Props addCommonFlowProperties(final ExecutableFlow flow) {
diff --git a/src/java/azkaban/utils/StringUtils.java b/src/java/azkaban/utils/StringUtils.java
index 90fd688..6684b00 100644
--- a/src/java/azkaban/utils/StringUtils.java
+++ b/src/java/azkaban/utils/StringUtils.java
@@ -15,6 +15,7 @@
  */
 package azkaban.utils;
 
+import java.util.Collection;
 import java.util.List;
 
 public class StringUtils {
@@ -45,7 +46,7 @@ public class StringUtils {
 	 * @param delimiter
 	 * @return
 	 */
-	public static String join(List<String> list, String delimiter) {
+	public static String join(Collection<String> list, String delimiter) {
 		StringBuffer buffer = new StringBuffer();
 		for (String str: list) {
 			buffer.append(str);
@@ -54,4 +55,5 @@ public class StringUtils {
 		
 		return buffer.toString();
 	}
+
 }