You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@metron.apache.org by ma...@apache.org on 2017/04/17 06:17:05 UTC

incubator-metron git commit: METRON-830 Adding StringFunctions to Stellar - chop, prependifmissing, appendifmissing and countmatches (anandsubbu via mattf-horton) closes apache/incubator-metron#516

Repository: incubator-metron
Updated Branches:
  refs/heads/master d30d8b9df -> e6992d568


METRON-830 Adding StringFunctions to Stellar - chop, prependifmissing, appendifmissing and countmatches (anandsubbu via mattf-horton) closes apache/incubator-metron#516


Project: http://git-wip-us.apache.org/repos/asf/incubator-metron/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-metron/commit/e6992d56
Tree: http://git-wip-us.apache.org/repos/asf/incubator-metron/tree/e6992d56
Diff: http://git-wip-us.apache.org/repos/asf/incubator-metron/diff/e6992d56

Branch: refs/heads/master
Commit: e6992d56816c37f8f63274a5a1ce33d37d69dc68
Parents: d30d8b9
Author: anandsubbu <as...@hortonworks.com>
Authored: Sun Apr 16 23:13:38 2017 -0700
Committer: mattf <ma...@apache.org>
Committed: Sun Apr 16 23:13:38 2017 -0700

----------------------------------------------------------------------
 metron-platform/metron-common/README.md         |  44 ++-
 .../common/dsl/functions/StringFunctions.java   | 132 ++++++++-
 .../dsl/functions/StringFunctionsTest.java      | 267 +++++++++++++++++--
 3 files changed, 417 insertions(+), 26 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/e6992d56/metron-platform/metron-common/README.md
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/README.md b/metron-platform/metron-common/README.md
index 8b910f8..de3a1b7 100644
--- a/metron-platform/metron-common/README.md
+++ b/metron-platform/metron-common/README.md
@@ -99,11 +99,15 @@ In the core language functions, we support basic functional programming primitiv
 |                                                                                                    |
 | ----------                                                                                         |
 | [ `ABS`](../../metron-analytics/metron-statistics#abs)                                             |
+| [ `APPEND_IF_MISSING`](#append_if_missing)                                                         |
 | [ `BIN`](../../metron-analytics/metron-statistics#bin)                                             |
 | [ `BLOOM_ADD`](#bloom_add)                                                                         |
 | [ `BLOOM_EXISTS`](#bloom_exists)                                                                   |
 | [ `BLOOM_INIT`](#bloom_init)                                                                       |
 | [ `BLOOM_MERGE`](#bloom_merge)                                                                     |
+| [ `CHOP`](#chop)                                                                                   |
+| [ `CHOMP`](#chomp)                                                                                 |
+| [ `COUNT_MATCHES`](#count_matches)                                                                 |
 | [ `DAY_OF_MONTH`](#day_of_month)                                                                   |
 | [ `DAY_OF_WEEK`](#day_of_week)                                                                     |
 | [ `DAY_OF_YEAR`](#day_of_year)                                                                     |
@@ -145,9 +149,10 @@ In the core language functions, we support basic functional programming primitiv
 | [ `MAP`](#map)                                                                       |
 | [ `MAP_EXISTS`](#map_exists)                                                                       |
 | [ `MONTH`](#month)                                                                                 |
+| [ `PREPEND_IF_MISSING`](#prepend_if_missing)                                                       |
 | [ `PROFILE_GET`](#profile_get)                                                                     |
-| [ `PROFILE_FIXED`](#profile_fixed)                                                                     |
-| [ `PROFILE_WINDOW`](#profile_window)                                                                     |
+| [ `PROFILE_FIXED`](#profile_fixed)                                                                 |
+| [ `PROFILE_WINDOW`](#profile_window)                                                               |
 | [ `PROTOCOL_TO_NAME`](#protocol_to_name)                                                           |
 | [ `REDUCE`](#reduce)                                                                   |
 | [ `REGEXP_MATCH`](#regexp_match)                                                                   |
@@ -192,6 +197,14 @@ In the core language functions, we support basic functional programming primitiv
 | [ `WEEK_OF_YEAR`](#week_of_year)                                                                   |
 | [ `YEAR`](#year)                                                                                   |
 
+### `APPEND_IF_MISSING`
+  * Description: Appends the suffix to the end of the string if the string does not already end with any of the suffixes.
+  * Input:
+    * string - The string to be appended.
+    * suffix - The string suffix to append to the end of the string.
+    * additionalsuffix - Optional - Additional string suffix that is a valid terminator.
+  * Returns: A new String if prefix was prepended, the same string otherwise.
+
 ### `BLOOM_ADD`
   * Description: Adds an element to the bloom filter passed in
   * Input:
@@ -219,6 +232,25 @@ In the core language functions, we support basic functional programming primitiv
     * bloomfilters - A list of bloom filters to merge
   * Returns: Bloom Filter or null if the list is empty
 
+### `CHOP`
+  * Description: Remove the last character from a String
+  * Input:
+    * string - the String to chop last character from, may be null
+  * Returns: String without last character, null if null String input
+
+### `CHOMP`
+  * Description: Removes one newline from end of a String if it's there, otherwise leave it alone. A newline is "\n", "\r", or "\r\n"
+  * Input:
+    * string - the String to chomp a newline from, may be null
+  * Returns: String without newline, null if null String input
+
+### `COUNT_MATCHES`
+  * Description: Counts how many times the substring appears in the larger string.
+  * Input:
+    * string - the CharSequence to check, may be null.
+    * substring/character - the substring or character to count, may be null.
+  * Returns: the number of non-overlapping occurrences, 0 if either CharSequence is null.
+
 ### `DAY_OF_MONTH`
   * Description: The numbered day within the month.  The first day within the month has a value of 1.
   * Input:
@@ -480,6 +512,14 @@ In the core language functions, we support basic functional programming primitiv
     * dateTime - The datetime as a long representing the milliseconds since unix epoch
   * Returns: The current month (0-based).
 
+### `PREPEND_IF_MISSING`
+  * Description: Prepends the prefix to the start of the string if the string does not already start with any of the prefixes.
+  * Input:
+    * string - The string to be prepended.
+    * prefix - The string prefix to prepend to the start of the string.
+    * additionalprefix - Optional - Additional string prefix that is valid.
+  * Returns: A new String if prefix was prepended, the same string otherwise.
+
 ### `PROFILE_GET`
   * Description: Retrieves a series of values from a stored profile.
   * Input:

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/e6992d56/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/StringFunctions.java
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/StringFunctions.java b/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/StringFunctions.java
index 1086da3..1ae790d 100644
--- a/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/StringFunctions.java
+++ b/metron-platform/metron-common/src/main/java/org/apache/metron/common/dsl/functions/StringFunctions.java
@@ -21,7 +21,7 @@ package org.apache.metron.common.dsl.functions;
 import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.collect.Iterables;
-import org.apache.commons.lang.StringUtils;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.metron.common.dsl.BaseStellarFunction;
 import org.apache.metron.common.dsl.ParseException;
 import org.apache.metron.common.dsl.Stellar;
@@ -343,4 +343,134 @@ public class StringFunctions {
       return String.format(format, formatArgs);
     }
   }
+
+  @Stellar( name="CHOMP"
+          , description = "Removes one newline from end of a String if it's there, otherwise leave it alone. A newline is \"\\n\", \"\\r\", or \"\\r\\n\""
+          , params = { "the String to chomp a newline from, may be null"}
+          , returns = "String without newline, null if null String input"
+  )
+  public static class Chomp extends BaseStellarFunction {
+
+    @Override
+    public Object apply(List<Object> strings) {
+
+      if(strings == null || strings.size() == 0 ) {
+        throw new IllegalArgumentException("[CHOMP] missing argument: string to be chopped");
+      }
+      String var = strings.get(0) == null?null: (String) strings.get(0);
+      if(var == null) {
+        return null;
+      }
+      else if(var.length() == 0) {
+        return var;
+      }
+      else {
+        return StringUtils.chomp(var);
+      }
+    }
+  }
+  @Stellar( name="CHOP"
+          , description = "Remove the last character from a String"
+          , params = { "the String to chop last character from, may be null"}
+          , returns = "String without last character, null if null String input"
+  )
+  public static class Chop extends BaseStellarFunction {
+
+    @Override
+    public Object apply(List<Object> strings) {
+
+      if(strings == null || strings.size() == 0 ) {
+        throw new IllegalArgumentException("[CHOP] missing argument: string to be chopped");
+      }
+      String var = strings.get(0) == null?null: (String) strings.get(0);
+      if(var == null) {
+        return null;
+      }
+      else if(var.length() == 0) {
+        return var;
+      }
+      else {
+        return StringUtils.chop(var);
+      }
+    }
+  }
+
+  @Stellar( name = "PREPEND_IF_MISSING"
+          , description = "Prepends the prefix to the start of the string if the string does not already start with any of the prefixes"
+          , params = {
+          "str - The string."
+          , "prefix - The string prefix to prepend to the start of the string"
+          , "additionalprefix - Optional - Additional string prefix that is valid"
+  }
+          , returns = "A new String if prefix was prepended, the same string otherwise."
+  )
+  public static class PrependIfMissing extends BaseStellarFunction {
+
+    @Override
+    public Object apply(List<Object> strings) {
+
+      String prefixed;
+      switch (strings.size()) {
+        case 2: prefixed = StringUtils.prependIfMissing((String) strings.get(0), (String) strings.get(1));
+          break;
+        case 3: prefixed = StringUtils.prependIfMissing((String) strings.get(0), (String) strings.get(1), (String) strings.get(2));
+          break;
+        default: throw new IllegalArgumentException("[PREPEND_IF_MISSING] incorrect arguments: " + strings.toString() + "\nUsage: PREPEND_IF_MISSING <String> <prefix> [<prefix>...]");
+      }
+      return prefixed;
+    }
+  }
+
+  @Stellar( name = "APPEND_IF_MISSING"
+          , description = "Appends the suffix to the end of the string if the string does not already end with any of the suffixes"
+          , params = {
+          "str - The string."
+          , "suffix - The string suffix to append to the end of the string"
+          , "additionalsuffix - Optional - Additional string suffix that is a valid terminator"
+  }
+          , returns = "A new String if suffix was appended, the same string otherwise."
+  )
+  public static class AppendIfMissing extends BaseStellarFunction {
+
+    @Override
+    public Object apply(List<Object> strings) {
+
+      String suffixed;
+      switch (strings.size()) {
+        case 2:
+          suffixed = StringUtils.appendIfMissing((String) strings.get(0), (String) strings.get(1));
+          break;
+        case 3:
+          suffixed = StringUtils.appendIfMissing((String) strings.get(0), (String) strings.get(1), (String) strings.get(2));
+          break;
+        default:
+          throw new IllegalArgumentException("[APPEND_IF_MISSING] incorrect arguments. Usage: APPEND_IF_MISSING <String> <prefix> [<prefix>...]");
+      }
+      return suffixed;
+    }
+  }
+
+  @Stellar( name = "COUNT_MATCHES"
+          , description = "Counts how many times the substring appears in the larger string"
+          , params = {
+          "str - the CharSequence to check, may be null"
+          , "sub - the substring to count, may be null"
+  }
+          , returns = "the number of non-overlapping occurrences, 0 if either CharSequence is null"
+  )
+  public static class CountMatches extends BaseStellarFunction {
+
+    @Override
+    public Object apply(List<Object> strings) {
+
+      if(strings.size() != 2) {
+        throw new IllegalArgumentException("[COUNT_MATCHES] incorrect arguments. Usage: COUNT_MATCHES <String> <substring>");
+      }
+
+      int matchcount;
+      matchcount = StringUtils.countMatches((String) strings.get(0), (String) strings.get(1));
+      return matchcount;
+    }
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-metron/blob/e6992d56/metron-platform/metron-common/src/test/java/org/apache/metron/common/dsl/functions/StringFunctionsTest.java
----------------------------------------------------------------------
diff --git a/metron-platform/metron-common/src/test/java/org/apache/metron/common/dsl/functions/StringFunctionsTest.java b/metron-platform/metron-common/src/test/java/org/apache/metron/common/dsl/functions/StringFunctionsTest.java
index d8854f8..8899535 100644
--- a/metron-platform/metron-common/src/test/java/org/apache/metron/common/dsl/functions/StringFunctionsTest.java
+++ b/metron-platform/metron-common/src/test/java/org/apache/metron/common/dsl/functions/StringFunctionsTest.java
@@ -67,40 +67,40 @@ public class StringFunctionsTest {
   }
 
   @Test
-  public void testLeftRightFills() throws Exception{
+  public void testLeftRightFills() throws Exception {
     final Map<String, Object> variableMap = new HashMap<String, Object>() {{
       put("foo", null);
       put("bar", null);
-      put("notInt","oh my");
+      put("notInt", "oh my");
     }};
 
     //LEFT
-    Object left = run("FILL_LEFT('123','X', 10)",new HashedMap());
+    Object left = run("FILL_LEFT('123','X', 10)", new HashedMap());
     Assert.assertNotNull(left);
-    Assert.assertEquals(10,((String)left).length());
-    Assert.assertEquals("XXXXXXX123",(String)left);
+    Assert.assertEquals(10, ((String) left).length());
+    Assert.assertEquals("XXXXXXX123", (String) left);
 
     //RIGHT
     Object right = run("FILL_RIGHT('123','X', 10)", new HashedMap());
     Assert.assertNotNull(right);
-    Assert.assertEquals(10,((String)right).length());
-    Assert.assertEquals("123XXXXXXX",(String)right);
+    Assert.assertEquals(10, ((String) right).length());
+    Assert.assertEquals("123XXXXXXX", (String) right);
 
     //INPUT ALREADY LENGTH
     Object same = run("FILL_RIGHT('123','X', 3)", new HashedMap());
-    Assert.assertEquals(3,((String)same).length());
-    Assert.assertEquals("123",(String)same);
+    Assert.assertEquals(3, ((String) same).length());
+    Assert.assertEquals("123", (String) same);
 
     //INPUT BIGGER THAN LENGTH
     Object tooBig = run("FILL_RIGHT('1234567890','X', 3)", new HashedMap());
-    Assert.assertEquals(10,((String)tooBig).length());
-    Assert.assertEquals("1234567890",(String)tooBig);
+    Assert.assertEquals(10, ((String) tooBig).length());
+    Assert.assertEquals("1234567890", (String) tooBig);
 
     //NULL VARIABLES
     boolean thrown = false;
-    try{
+    try {
       run("FILL_RIGHT('123',foo,bar)", variableMap);
-    }catch(ParseException pe) {
+    } catch (ParseException pe) {
       thrown = true;
       Assert.assertTrue(pe.getMessage().contains("are both required"));
     }
@@ -108,9 +108,9 @@ public class StringFunctionsTest {
     thrown = false;
 
     // NULL LENGTH
-    try{
+    try {
       run("FILL_RIGHT('123','X',bar)", variableMap);
-    }catch(ParseException pe) {
+    } catch (ParseException pe) {
       thrown = true;
       Assert.assertTrue(pe.getMessage().contains("are both required"));
     }
@@ -118,9 +118,9 @@ public class StringFunctionsTest {
     thrown = false;
 
     // NULL FILL
-    try{
+    try {
       run("FILL_RIGHT('123',foo, 7)", variableMap);
-    }catch(ParseException pe) {
+    } catch (ParseException pe) {
       thrown = true;
       Assert.assertTrue(pe.getMessage().contains("are both required"));
     }
@@ -130,7 +130,7 @@ public class StringFunctionsTest {
     // NON INTEGER LENGTH
     try {
       run("FILL_RIGHT('123','X', 'z' )", new HashedMap());
-    }catch(ParseException pe){
+    } catch (ParseException pe) {
       thrown = true;
       Assert.assertTrue(pe.getMessage().contains("not a valid Integer"));
     }
@@ -140,7 +140,7 @@ public class StringFunctionsTest {
     // EMPTY STRING PAD
     try {
       Object returnValue = run("FILL_RIGHT('123','', 10 )", new HashedMap());
-    }catch(ParseException pe) {
+    } catch (ParseException pe) {
       thrown = true;
       Assert.assertTrue(pe.getMessage().contains("cannot be an empty"));
     }
@@ -150,7 +150,7 @@ public class StringFunctionsTest {
     //MISSING LENGTH PARAMETER
     try {
       run("FILL_RIGHT('123',foo)", variableMap);
-    }catch(ParseException pe){
+    } catch (ParseException pe) {
       thrown = true;
       Assert.assertTrue(pe.getMessage().contains("expects three"));
     }
@@ -160,8 +160,8 @@ public class StringFunctionsTest {
   @Test
   public void shannonEntropyTest() throws Exception {
     //test empty string
-    Assert.assertEquals(0.0, (Double)run("STRING_ENTROPY('')", new HashMap<>()), 0.0);
-    Assert.assertEquals(0.0, (Double)run("STRING_ENTROPY(foo)", ImmutableMap.of("foo", "")), 0.0);
+    Assert.assertEquals(0.0, (Double) run("STRING_ENTROPY('')", new HashMap<>()), 0.0);
+    Assert.assertEquals(0.0, (Double) run("STRING_ENTROPY(foo)", ImmutableMap.of("foo", "")), 0.0);
 
     /*
     Now consider the string aaaaaaaaaabbbbbccccc or 10 a's followed by 5 b's and 5 c's.
@@ -173,7 +173,7 @@ public class StringFunctionsTest {
       -p(a)*log_2(p(a)) - p(b)*log_2(p(b)) - p(c)*log_2(p(c)) =
       -0.5*-1 - 0.25*-2 - 0.25*-2 = 1.5
      */
-    Assert.assertEquals(1.5, (Double)run("STRING_ENTROPY(foo)", ImmutableMap.of("foo", "aaaaaaaaaabbbbbccccc")), 0.0);
+    Assert.assertEquals(1.5, (Double) run("STRING_ENTROPY(foo)", ImmutableMap.of("foo", "aaaaaaaaaabbbbbccccc")), 0.0);
   }
 
   @Test
@@ -207,4 +207,225 @@ public class StringFunctionsTest {
   public void testFormatWithMissingArguments() throws Exception {
     run("FORMAT('missing arg: %d')", Collections.emptyMap());
   }
+
+
+  /**
+   * CHOMP StringFunction
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testChomp() throws Exception {
+    Assert.assertEquals("abc",  run("CHOMP('abc')", new HashedMap()));
+    Assert.assertEquals("abc",  run("CHOMP(msg)", ImmutableMap.of("msg", "abc\r\n")));
+    Assert.assertEquals("",     run("CHOMP(msg)", ImmutableMap.of("msg", "\n")));
+    Assert.assertEquals("",     run("CHOMP('')", new HashedMap()));
+    Assert.assertEquals(null,   run("CHOMP(msg)", new HashedMap()));
+    Assert.assertEquals(null,   run("CHOMP(null)", new HashedMap()));
+
+    // No input
+    boolean thrown = false;
+    try {
+      run("CHOMP()", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("missing argument"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Integer input
+    try {
+      run("CHOMP(123)", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("cannot be cast"));
+    }
+    Assert.assertTrue(thrown);
+
+  }
+
+  /**
+   * CHOP StringFunction
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testChop() throws Exception {
+    Assert.assertEquals("ab",   run("CHOP('abc')", new HashedMap()));
+    Assert.assertEquals(null,   run("CHOP(null)", new HashedMap()));
+    Assert.assertEquals(null,   run("CHOP(msg)", new HashedMap()));
+    Assert.assertEquals("abc",  run("CHOP(msg)", ImmutableMap.of("msg", "abc\r\n")));
+    Assert.assertEquals("",     run("CHOP(msg)", ImmutableMap.of("msg", "")));
+    Assert.assertEquals("",     run("CHOP(msg)", ImmutableMap.of("msg", "\n")));
+    Assert.assertEquals("",     run("CHOP('')", new HashedMap()));
+
+    // No input
+    boolean thrown = false;
+    try {
+      run("CHOP()", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("missing argument"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Integer input
+    try {
+      run("CHOP(123)", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("cannot be cast"));
+    }
+    Assert.assertTrue(thrown);
+
+  }
+
+  /**
+   * PREPEND_IF_MISSING StringFunction
+   */
+  @Test
+  public void testPrependIfMissing() throws Exception {
+    Assert.assertEquals("xyzabc",     run("PREPEND_IF_MISSING('abc', 'xyz')", new HashedMap()));
+    Assert.assertEquals("xyzXYZabc",  run("PREPEND_IF_MISSING('XYZabc', 'xyz', 'mno')", new HashedMap()));
+    Assert.assertEquals("mnoXYZabc",  run("PREPEND_IF_MISSING('mnoXYZabc', 'xyz', 'mno')", new HashedMap()));
+    Assert.assertEquals(null,         run("PREPEND_IF_MISSING(null, null, null)", new HashedMap()));
+    Assert.assertEquals("xyz",        run("PREPEND_IF_MISSING('', 'xyz', null)", new HashedMap()));
+
+    // No input
+    boolean thrown = false;
+    try {
+      run("PREPEND_IF_MISSING()", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Incorrect number of arguments - 1
+    try {
+      run("PREPEND_IF_MISSING('abc')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Incorrect number of arguments - 2
+    try {
+      run("PREPEND_IF_MISSING('abc', 'def', 'ghi', 'jkl')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Integer input
+    try {
+      run("PREPEND_IF_MISSING(123, 'abc')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("cannot be cast"));
+    }
+    Assert.assertTrue(thrown);
+
+  }
+
+  /**
+   * APPEND_IF_MISSING StringFunction
+   */
+  @Test
+  public void testAppendIfMissing() throws Exception {
+    Assert.assertEquals("apachemetron",   run("APPEND_IF_MISSING('apache', 'metron')", new HashedMap()));
+    Assert.assertEquals("abcXYZxyz",      run("APPEND_IF_MISSING('abcXYZ', 'xyz', 'mno')", new HashedMap()));
+    Assert.assertEquals(null,             run("APPEND_IF_MISSING(null, null, null)", new HashedMap()));
+    Assert.assertEquals("xyz",            run("APPEND_IF_MISSING('', 'xyz', null)", new HashedMap()));
+
+    // No input
+    boolean thrown = false;
+    try {
+      run("APPEND_IF_MISSING()", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Incorrect number of arguments - 1
+    try {
+      run("APPEND_IF_MISSING('abc')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Incorrect number of arguments - 2
+    try {
+      run("APPEND_IF_MISSING('abc', 'def', 'ghi', 'jkl')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Integer input
+    try {
+      run("APPEND_IF_MISSING(123, 'abc')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("cannot be cast"));
+    }
+    Assert.assertTrue(thrown);
+
+  }
+
+  /**
+   * COUNT_MATCHES StringFunction
+   */
+  @Test
+  public void testCountMatches() throws Exception {
+    Assert.assertEquals(0, (int) run("COUNT_MATCHES(null, '*')", new HashedMap()));
+    Assert.assertEquals(2, (int) run("COUNT_MATCHES('apachemetron', 'e')", new HashedMap()));
+    Assert.assertEquals(2, (int) run("COUNT_MATCHES('anand', 'an')", new HashedMap()));
+    Assert.assertEquals(0, (int) run("COUNT_MATCHES('abcd', null)", new HashedMap()));
+
+    // No input
+    boolean thrown = false;
+    try {
+      run("COUNT_MATCHES()", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Incorrect number of arguments - 1
+    try {
+      run("COUNT_MATCHES('abc')", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("incorrect arguments"));
+    }
+    Assert.assertTrue(thrown);
+    thrown = false;
+
+    // Integer input
+    try {
+      run("COUNT_MATCHES(123, 456)", Collections.emptyMap());
+    } catch (ParseException pe) {
+      thrown = true;
+      Assert.assertTrue(pe.getMessage().contains("cannot be cast"));
+    }
+    Assert.assertTrue(thrown);
+
+  }
 }