MINIFICPP-331 Implemented string replacement functions

This closes #226.

Signed-off-by: Marc Parisi <phrocker@apache.org>
diff --git a/extensions/expression-language/Expression.cpp b/extensions/expression-language/Expression.cpp
index 806ff6a..2220434 100644
--- a/extensions/expression-language/Expression.cpp
+++ b/extensions/expression-language/Expression.cpp
@@ -19,6 +19,7 @@
 #include <iostream>
 
 #include <expression/Expression.h>
+#include <regex>
 #include "Driver.h"
 
 namespace org {
@@ -96,6 +97,55 @@
   return args[0].substr(last_pos + args[1].length());
 }
 
+#ifdef EXPRESSION_LANGUAGE_USE_REGEX
+
+std::string expr_replace(const std::vector<std::string> &args) {
+  std::string result = args[0];
+  const std::string find = args[1];
+  const std::string replace = args[2];
+
+  std::string::size_type match_pos = 0;
+  match_pos = result.find(find, match_pos);
+
+  while (match_pos != std::string::npos) {
+    result.replace(match_pos, find.size(), replace);
+    match_pos = result.find(find, match_pos + replace.size());
+  }
+
+  return result;
+}
+
+std::string expr_replaceFirst(const std::vector<std::string> &args) {
+  std::string result = args[0];
+  const std::regex find(args[1]);
+  const std::string replace = args[2];
+  return std::regex_replace(result, find, replace, std::regex_constants::format_first_only);
+}
+
+std::string expr_replaceAll(const std::vector<std::string> &args) {
+  std::string result = args[0];
+  const std::regex find(args[1]);
+  const std::string replace = args[2];
+  return std::regex_replace(result, find, replace);
+}
+
+std::string expr_replaceNull(const std::vector<std::string> &args) {
+  if (args[0].empty()) {
+    return args[1];
+  } else {
+    return args[0];
+  }
+}
+
+std::string expr_replaceEmpty(const std::vector<std::string> &args) {
+  std::string result = args[0];
+  const std::regex find("^[ \n\r\t]*$");
+  const std::string replace = args[1];
+  return std::regex_replace(result, find, replace);
+}
+
+#endif  // EXPRESSION_LANGUAGE_USE_REGEX
+
 template<std::string T(const std::vector<std::string> &)>
 Expression make_dynamic_function_incomplete(const std::string &function_name,
                                             const std::vector<Expression> &args,
@@ -149,6 +199,18 @@
     return make_dynamic_function_incomplete<expr_substringAfter>(function_name, args, 2);
   } else if (function_name == "substringAfterLast") {
     return make_dynamic_function_incomplete<expr_substringAfterLast>(function_name, args, 2);
+#ifdef EXPRESSION_LANGUAGE_USE_REGEX
+  } else if (function_name == "replace") {
+    return make_dynamic_function_incomplete<expr_replace>(function_name, args, 2);
+  } else if (function_name == "replaceFirst") {
+    return make_dynamic_function_incomplete<expr_replaceFirst>(function_name, args, 2);
+  } else if (function_name == "replaceAll") {
+    return make_dynamic_function_incomplete<expr_replaceAll>(function_name, args, 2);
+  } else if (function_name == "replaceNull") {
+    return make_dynamic_function_incomplete<expr_replaceNull>(function_name, args, 1);
+  } else if (function_name == "replaceEmpty") {
+    return make_dynamic_function_incomplete<expr_replaceEmpty>(function_name, args, 1);
+#endif  // EXPRESSION_LANGUAGE_USE_REGEX
   } else {
     std::string msg("Unknown expression function: ");
     msg.append(function_name);
diff --git a/extensions/expression-language/impl/expression/Expression.h b/extensions/expression-language/impl/expression/Expression.h
index 492bc0b..e245370 100644
--- a/extensions/expression-language/impl/expression/Expression.h
+++ b/extensions/expression-language/impl/expression/Expression.h
@@ -18,6 +18,13 @@
 #ifndef NIFI_MINIFI_CPP_EXPRESSION_H
 #define NIFI_MINIFI_CPP_EXPRESSION_H
 
+#define EXPRESSION_LANGUAGE_USE_REGEX
+
+// Disable regex in EL for incompatible compilers
+#if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 9)
+#undef EXPRESSION_LANGUAGE_USE_REGEX
+#endif
+
 #include <core/FlowFile.h>
 
 #include <string>
diff --git a/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp b/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp
index 0af816d..8660af5 100644
--- a/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp
+++ b/libminifi/test/expression-language-tests/ExpressionLanguageTests.cpp
@@ -304,3 +304,103 @@
   flow_file_a->addAttribute("attr", "__flow_a_attr_value_a__");
   REQUIRE_THROWS_WITH(expr({flow_file_a}), "Attempted to call incomplete function");
 }
+
+#ifdef EXPRESSION_LANGUAGE_USE_REGEX
+
+TEST_CASE("Replace", "[expressionLanguageReplace]") {  // NOLINT
+  auto expr = expression::compile("${attr:replace('.', '_')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a brand new filename_txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace 2", "[expressionLanguageReplace2]") {  // NOLINT
+  auto expr = expression::compile("${attr:replace(' ', '.')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a.brand.new.filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace First", "[expressionLanguageReplaceFirst]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceFirst('a', 'the')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("the brand new filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace First Regex", "[expressionLanguageReplaceFirstRegex]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceFirst('[br]', 'g')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a grand new filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace All", "[expressionLanguageReplaceAll]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceAll('\\..*', '')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a brand new filename" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace All 2", "[expressionLanguageReplaceAll2]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceAll('a brand (new)', '$1')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("new filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace All 3", "[expressionLanguageReplaceAll3]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceAll('XYZ', 'ZZZ')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a brand new filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace Null", "[expressionLanguageReplaceNull]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceNull('abc')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a brand new filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace Null 2", "[expressionLanguageReplaceNull2]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceNull('abc')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr2", "a brand new filename.txt");
+  REQUIRE("abc" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace Empty", "[expressionLanguageReplaceEmpty]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceEmpty('abc')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "a brand new filename.txt");
+  REQUIRE("a brand new filename.txt" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace Empty 2", "[expressionLanguageReplaceEmpty2]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceEmpty('abc')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr", "  \t  \r  \n  ");
+  REQUIRE("abc" == expr({flow_file_a}));
+}
+
+TEST_CASE("Replace Empty 3", "[expressionLanguageReplaceEmpty2]") {  // NOLINT
+  auto expr = expression::compile("${attr:replaceEmpty('abc')}");
+
+  auto flow_file_a = std::make_shared<MockFlowFile>();
+  flow_file_a->addAttribute("attr2", "test");
+  REQUIRE("abc" == expr({flow_file_a}));
+}
+
+#endif  // EXPRESSION_LANGUAGE_USE_REGEX