diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RouteText.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RouteText.java index 6872e415a9..edec3c1de4 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RouteText.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RouteText.java @@ -31,11 +31,15 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReference; import java.util.regex.Matcher; import java.util.regex.Pattern; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.CacheBuilder; import org.apache.commons.lang3.StringUtils; + import org.apache.nifi.annotation.behavior.DynamicProperty; import org.apache.nifi.annotation.behavior.DynamicRelationship; import org.apache.nifi.annotation.behavior.EventDriven; @@ -209,6 +213,24 @@ public class RouteText extends AbstractProcessor { private volatile Map propertyMap = new HashMap<>(); private volatile Pattern groupingRegex = null; + @VisibleForTesting + final static int PATTERNS_CACHE_MAXIMUM_ENTRIES = 1024; + + /** + * LRU cache for the compiled patterns. The size of the cache is determined by the value of + * {@link #PATTERNS_CACHE_MAXIMUM_ENTRIES}. + */ + @VisibleForTesting + final ConcurrentMap patternsCache = CacheBuilder.newBuilder() + .maximumSize(PATTERNS_CACHE_MAXIMUM_ENTRIES) + .build() + .asMap(); + + private Pattern cachedCompiledPattern(final String regex, final boolean ignoreCase) { + return patternsCache.computeIfAbsent(regex, + r -> ignoreCase ? Pattern.compile(r, Pattern.CASE_INSENSITIVE) : Pattern.compile(r)); + } + @Override protected void init(final ProcessorInitializationContext context) { final Set set = new HashSet<>(); @@ -249,6 +271,10 @@ public class RouteText extends AbstractProcessor { @Override public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) { + if (descriptor.equals(IGNORE_CASE) && !newValue.equals(oldValue)) { + patternsCache.clear(); + } + if (descriptor.equals(ROUTE_STRATEGY)) { configuredRouteStrategy = newValue; } else { @@ -384,11 +410,7 @@ public class RouteText extends AbstractProcessor { for (final Map.Entry entry : propMap.entrySet()) { final String value = entry.getValue().evaluateAttributeExpressions(originalFlowFile).getValue(); - Pattern compiledRegex = null; - if (compileRegex) { - compiledRegex = ignoreCase ? Pattern.compile(value, Pattern.CASE_INSENSITIVE) : Pattern.compile(value); - } - propValueMap.put(entry.getKey(), compileRegex ? compiledRegex : value); + propValueMap.put(entry.getKey(), compileRegex ? cachedCompiledPattern(value, ignoreCase) : value); } } @@ -435,7 +457,7 @@ public class RouteText extends AbstractProcessor { int propertiesThatMatchedLine = 0; for (final Map.Entry entry : propValueMap.entrySet()) { - boolean lineMatchesProperty = lineMatches(matchLine, entry.getValue(), context.getProperty(MATCH_STRATEGY).getValue(), ignoreCase, originalFlowFile, variables); + boolean lineMatchesProperty = lineMatches(matchLine, entry.getValue(), matchStrategy, ignoreCase, originalFlowFile, variables); if (lineMatchesProperty) { propertiesThatMatchedLine++; } diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRouteText.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRouteText.java index 71bd83b371..32048a5efc 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRouteText.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRouteText.java @@ -28,6 +28,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import com.google.common.collect.ImmutableMap; import org.apache.nifi.processor.Relationship; import org.apache.nifi.util.MockFlowFile; import org.apache.nifi.util.TestRunner; @@ -763,6 +764,40 @@ public class TestRouteText { outOriginal.assertContentEquals(Paths.get("src/test/resources/TestXml/XmlBundle.xsd")); } + @Test + public void testPatternCache() throws IOException { + final RouteText routeText = new RouteText(); + final TestRunner runner = TestRunners.newTestRunner(routeText); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.MATCHES_REGULAR_EXPRESSION); + runner.setProperty("simple", ".*(${someValue}).*"); + + runner.enqueue("some text", ImmutableMap.of("someValue", "a value")); + runner.enqueue("some other text", ImmutableMap.of("someValue", "a value")); + runner.run(2); + + assertEquals("Expected 1 elements in the cache for the patterns, got" + + routeText.patternsCache.size(), 1, routeText.patternsCache.size()); + + for (int i = 0; i < RouteText.PATTERNS_CACHE_MAXIMUM_ENTRIES * 2; ++i) { + String iString = Long.toString(i); + runner.enqueue("some text with " + iString + "in it", + ImmutableMap.of("someValue", iString)); + runner.run(); + } + + assertEquals("Expected " + RouteText.PATTERNS_CACHE_MAXIMUM_ENTRIES + + " elements in the cache for the patterns, got" + routeText.patternsCache.size(), + RouteText.PATTERNS_CACHE_MAXIMUM_ENTRIES, routeText.patternsCache.size()); + + runner.assertTransferCount("simple", RouteText.PATTERNS_CACHE_MAXIMUM_ENTRIES * 2); + runner.assertTransferCount("unmatched", 2); + runner.assertTransferCount("original", RouteText.PATTERNS_CACHE_MAXIMUM_ENTRIES * 2 + 2); + + runner.setProperty(RouteText.IGNORE_CASE, "true"); + assertEquals("Pattern cache is not cleared after changing IGNORE_CASE", 0, routeText.patternsCache.size()); + } + + public static int countLines(String str) { if (str == null || str.isEmpty()) { return 0;