From 3e538d9007bd1e67702a31cff71b9bcba85a43ae Mon Sep 17 00:00:00 2001 From: Joseph Percivall Date: Wed, 4 Nov 2015 15:44:57 -0500 Subject: [PATCH] NIFI-1083 Created a processor that routes lines of text based on different matching and routing strategies Signed-off-by: Mark Payne --- .../nifi/processors/standard/RouteText.java | 439 +++++++++++++ .../standard/util/NLKBufferedReader.java | 2 +- .../org.apache.nifi.processor.Processor | 1 + .../processors/standard/TestRouteText.java | 590 ++++++++++++++++++ .../src/test/resources/simple.jpg | Bin 0 -> 25248 bytes 5 files changed, 1031 insertions(+), 1 deletion(-) create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RouteText.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRouteText.java create mode 100644 nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/simple.jpg 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 new file mode 100644 index 0000000000..81e879b848 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/RouteText.java @@ -0,0 +1,439 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.processors.standard; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.regex.Pattern; + +import org.apache.nifi.annotation.behavior.DynamicProperty; +import org.apache.nifi.annotation.behavior.DynamicRelationship; +import org.apache.nifi.annotation.behavior.EventDriven; +import org.apache.nifi.annotation.behavior.SideEffectFree; +import org.apache.nifi.annotation.behavior.SupportsBatching; +import org.apache.nifi.annotation.documentation.CapabilityDescription; +import org.apache.nifi.annotation.documentation.Tags; +import org.apache.nifi.annotation.lifecycle.OnScheduled; +import org.apache.nifi.components.AllowableValue; +import org.apache.nifi.components.PropertyDescriptor; +import org.apache.nifi.components.PropertyValue; +import org.apache.nifi.components.ValidationContext; +import org.apache.nifi.components.ValidationResult; +import org.apache.nifi.components.Validator; +import org.apache.nifi.flowfile.FlowFile; +import org.apache.nifi.logging.ProcessorLog; +import org.apache.nifi.processor.AbstractProcessor; +import org.apache.nifi.processor.ProcessContext; +import org.apache.nifi.processor.ProcessSession; +import org.apache.nifi.processor.ProcessorInitializationContext; +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.processor.io.InputStreamCallback; +import org.apache.nifi.processor.io.OutputStreamCallback; +import org.apache.nifi.processor.util.StandardValidators; +import org.apache.nifi.processors.standard.util.NLKBufferedReader; + + +@EventDriven +@SideEffectFree +@SupportsBatching +@Tags({"attributes", "routing", "text", "regexp", "regex", "Regular Expression", "Expression Language"}) +@CapabilityDescription("Routes textual data based on a set of user-defined rules. Each line in an incoming FlowFile is compared against the values specified by user-defined Properties. " + + "The mechanism by which the text is compared to these user-defined properties is defined by the 'Matching Strategy'. The data is then routed according to these rules, routing " + + "each line of the text individually.") +@DynamicProperty(name = "Relationship Name", value = "value to match against", description = "Routes data that matches the value specified in the Dynamic Property Value to the " + + "Relationship specified in the Dynamic Property Key.") +@DynamicRelationship(name = "Name from Dynamic Property", description = "FlowFiles that match the Dynamic Property's value") +public class RouteText extends AbstractProcessor { + + public static final String ROUTE_ATTRIBUTE_KEY = "RouteText.Route"; + + private static final String routeAllMatchValue = "Route to 'matched' if line matches all conditions"; + private static final String routeAnyMatchValue = "Route to 'matched' if lines matches any condition"; + private static final String routePropertyNameValue = "Route to each matching Property Name"; + + private static final String startsWithValue = "Starts With"; + private static final String endsWithValue = "Ends With"; + private static final String containsValue = "Contains"; + private static final String equalsValue = "Equals"; + private static final String matchesRegularExpressionValue = "Matches Regular Expression"; + private static final String containsRegularExpressionValue = "Contains Regular Expression"; + + + public static final AllowableValue ROUTE_TO_MATCHING_PROPERTY_NAME = new AllowableValue(routePropertyNameValue, routePropertyNameValue, + "Lines will be routed to each relationship whose corresponding expression evaluates to 'true'"); + public static final AllowableValue ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH = new AllowableValue(routeAllMatchValue, routeAllMatchValue, + "Requires that all user-defined expressions evaluate to 'true' for the line to be considered a match"); + public static final AllowableValue ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES = new AllowableValue(routeAnyMatchValue, routeAnyMatchValue, + "Requires that at least one user-defined expression evaluate to 'true' for the line to be considered a match"); + + public static final AllowableValue STARTS_WITH = new AllowableValue(startsWithValue, startsWithValue, + "Match lines based on whether the line starts with the property value"); + public static final AllowableValue ENDS_WITH = new AllowableValue(endsWithValue, endsWithValue, + "Match lines based on whether the line ends with the property value"); + public static final AllowableValue CONTAINS = new AllowableValue(containsValue, containsValue, + "Match lines based on whether the line contains the property value"); + public static final AllowableValue EQUALS = new AllowableValue(equalsValue, equalsValue, + "Match lines based on whether the line equals the property value"); + public static final AllowableValue MATCHES_REGULAR_EXPRESSION = new AllowableValue(matchesRegularExpressionValue, matchesRegularExpressionValue, + "Match lines based on whether the line exactly matches the Regular Expression that is provided as the Property value"); + public static final AllowableValue CONTAINS_REGULAR_EXPRESSION = new AllowableValue(containsRegularExpressionValue, containsRegularExpressionValue, + "Match lines based on whether the line contains some text that matches the Regular Expression that is provided as the Property value"); + + + public static final PropertyDescriptor ROUTE_STRATEGY = new PropertyDescriptor.Builder() + .name("Routing Strategy") + .description("Specifies how to determine which Relationship(s) to use when evaluating the lines of incoming text against the 'Matching Strategy' and user-defined properties.") + .required(true) + .allowableValues(ROUTE_TO_MATCHING_PROPERTY_NAME, ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH, ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES) + .defaultValue(ROUTE_TO_MATCHING_PROPERTY_NAME.getValue()) + .dynamic(false) + .build(); + + public static final PropertyDescriptor MATCH_STRATEGY = new PropertyDescriptor.Builder() + .name("Matching Strategy") + .description("Specifies how to evaluate each line of incoming text against the user-defined properties.") + .required(true) + .allowableValues(STARTS_WITH, ENDS_WITH, CONTAINS, EQUALS, MATCHES_REGULAR_EXPRESSION, CONTAINS_REGULAR_EXPRESSION) + .dynamic(false) + .build(); + + public static final PropertyDescriptor TRIM_WHITESPACE = new PropertyDescriptor.Builder() + .name("Ignore Leading/Trailing Whitespace") + .description("Indicates whether or the whitespace at the beginning and end of the lines should be ignored when evaluating the line.") + .required(true) + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .defaultValue("true") + .dynamic(false) + .build(); + + static final PropertyDescriptor IGNORE_CASE = new PropertyDescriptor.Builder() + .name("Ignore Case") + .description("If true, capitalization will not be taken into account when comparing values. E.g., matching against 'HELLO' or 'hello' will have the same result.") + .expressionLanguageSupported(false) + .allowableValues("true", "false") + .defaultValue("false") + .required(true) + .build(); + + public static final PropertyDescriptor CHARACTER_SET = new PropertyDescriptor.Builder() + .name("Character Set") + .description("The Character Set in which the incoming text is encoded") + .required(true) + .addValidator(StandardValidators.CHARACTER_SET_VALIDATOR) + .defaultValue("UTF-8") + .build(); + + public static final Relationship REL_ORIGINAL = new Relationship.Builder() + .name("original") + .description("The original input file will be routed to this destination when the lines have been successfully routed to 1 or more relationships") + .build(); + public static final Relationship REL_NO_MATCH = new Relationship.Builder() + .name("unmatched") + .description("Data that does not satisfy the required user-defined rules will be routed to this Relationship") + .build(); + public static final Relationship REL_MATCH = new Relationship.Builder() + .name("matched") + .description("Data that satisfies the required user-defined rules will be routed to this Relationship") + .build(); + + private AtomicReference> relationships = new AtomicReference<>(); + private List properties; + private volatile String configuredRouteStrategy = ROUTE_STRATEGY.getDefaultValue(); + private volatile Set dynamicPropertyNames = new HashSet<>(); + + /** + * Cache of dynamic properties set during {@link #onScheduled(ProcessContext)} for quick access in + * {@link #onTrigger(ProcessContext, ProcessSession)} + */ + private volatile Map propertyMap = new HashMap<>(); + + @Override + protected void init(final ProcessorInitializationContext context) { + final Set set = new HashSet<>(); + set.add(REL_ORIGINAL); + set.add(REL_NO_MATCH); + relationships = new AtomicReference<>(set); + + final List properties = new ArrayList<>(); + properties.add(ROUTE_STRATEGY); + properties.add(MATCH_STRATEGY); + properties.add(CHARACTER_SET); + properties.add(TRIM_WHITESPACE); + properties.add(IGNORE_CASE); + this.properties = Collections.unmodifiableList(properties); + } + + @Override + public Set getRelationships() { + return relationships.get(); + } + + @Override + protected List getSupportedPropertyDescriptors() { + return properties; + } + + @Override + protected PropertyDescriptor getSupportedDynamicPropertyDescriptor(final String propertyDescriptorName) { + return new PropertyDescriptor.Builder() + .required(false) + .name(propertyDescriptorName) + .expressionLanguageSupported(true) + .addValidator(StandardValidators.NON_EMPTY_VALIDATOR) + .dynamic(true) + .build(); + } + + @Override + public void onPropertyModified(final PropertyDescriptor descriptor, final String oldValue, final String newValue) { + if (descriptor.equals(ROUTE_STRATEGY)) { + configuredRouteStrategy = newValue; + } else { + final Set newDynamicPropertyNames = new HashSet<>(dynamicPropertyNames); + if (newValue == null) { + newDynamicPropertyNames.remove(descriptor.getName()); + } else if (oldValue == null && descriptor.isDynamic()) { // new property + newDynamicPropertyNames.add(descriptor.getName()); + } + + this.dynamicPropertyNames = Collections.unmodifiableSet(newDynamicPropertyNames); + } + + // formulate the new set of Relationships + final Set allDynamicProps = this.dynamicPropertyNames; + final Set newRelationships = new HashSet<>(); + final String routeStrategy = configuredRouteStrategy; + if (ROUTE_TO_MATCHING_PROPERTY_NAME.equals(routeStrategy)) { + for (final String propName : allDynamicProps) { + newRelationships.add(new Relationship.Builder().name(propName).build()); + } + } else { + newRelationships.add(REL_MATCH); + } + + newRelationships.add(REL_NO_MATCH); + this.relationships.set(newRelationships); + } + + /** + * When this processor is scheduled, update the dynamic properties into the map + * for quick access during each onTrigger call + * + * @param context ProcessContext used to retrieve dynamic properties + */ + @OnScheduled + public void onScheduled(final ProcessContext context) { + final Map newPropertyMap = new HashMap<>(); + for (final PropertyDescriptor descriptor : context.getProperties().keySet()) { + if (!descriptor.isDynamic()) { + continue; + } + getLogger().debug("Adding new dynamic property: {}", new Object[] {descriptor}); + newPropertyMap.put(new Relationship.Builder().name(descriptor.getName()).build(), context.getProperty(descriptor)); + } + + this.propertyMap = newPropertyMap; + } + + @Override + protected Collection customValidate(ValidationContext validationContext) { + Collection results = new ArrayList<>(super.customValidate(validationContext)); + boolean dynamicProperty = false; + + final String matchStrategy = validationContext.getProperty(MATCH_STRATEGY).getValue(); + final boolean compileRegex = matchStrategy.equals(matchesRegularExpressionValue) || matchStrategy.equals(containsRegularExpressionValue); + Validator validator = null; + if (compileRegex) { + validator = StandardValidators.createRegexValidator(0, Integer.MAX_VALUE, true); + } + + Map allProperties = validationContext.getProperties(); + for (final PropertyDescriptor descriptor : allProperties.keySet()) { + if (descriptor.isDynamic()) { + dynamicProperty = true; + + if (compileRegex) { + ValidationResult validationResult = validator.validate(descriptor.getName(), validationContext.getProperty(descriptor).getValue(), validationContext); + if (validationResult != null) { + results.add(validationResult); + } + } + } + } + + if (!dynamicProperty) { + results.add(new ValidationResult.Builder().subject("Dynamic Properties") + .explanation("In order to route text there must be dynamic properties to match against").valid(false).build()); + } + + return results; + } + + @Override + public void onTrigger(final ProcessContext context, final ProcessSession session) { + final FlowFile originalFlowFile = session.get(); + if (originalFlowFile == null) { + return; + } + + final ProcessorLog logger = getLogger(); + final Charset charset = Charset.forName(context.getProperty(CHARACTER_SET).getValue()); + final boolean trim = context.getProperty(TRIM_WHITESPACE).asBoolean(); + final String routeStrategy = context.getProperty(ROUTE_STRATEGY).getValue(); + final String matchStrategy = context.getProperty(MATCH_STRATEGY).getValue(); + final boolean ignoreCase = context.getProperty(IGNORE_CASE).asBoolean(); + + final Map propMap = this.propertyMap; + final Map propValueMap = new HashMap<>(propMap.size()); + + final boolean compileRegex = matchStrategy.equals(matchesRegularExpressionValue) || matchStrategy.equals(containsRegularExpressionValue); + + 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); + } + + final Map flowFileMap = new HashMap<>(); + + session.read(originalFlowFile, new InputStreamCallback() { + @Override + public void process(final InputStream in) throws IOException { + try (final Reader inReader = new InputStreamReader(in,charset); + final NLKBufferedReader reader = new NLKBufferedReader(inReader)) { + + String line; + while ((line = reader.readLine()) != null) { + + int propertiesThatMatchedLine = 0; + for (final Map.Entry entry : propValueMap.entrySet()) { + + String matchLine = trim ? line.trim() : line; + boolean lineMatchesProperty = lineMatches(matchLine, entry.getValue(), context.getProperty(MATCH_STRATEGY).getValue(), ignoreCase); + if (lineMatchesProperty) { + propertiesThatMatchedLine++; + } + + if (lineMatchesProperty && ROUTE_TO_MATCHING_PROPERTY_NAME.getValue().equals(routeStrategy)) { + // route each individual line to each Relationship that matches. This one matches. + final Relationship relationship = entry.getKey(); + appendLine(session, flowFileMap, relationship, originalFlowFile, line, charset); + continue; + } + + // break as soon as possible to avoid calculating things we don't need to calculate. + if (lineMatchesProperty && ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES.getValue().equals(routeStrategy)) { + break; + } + + if (!lineMatchesProperty && ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH.getValue().equals(routeStrategy)) { + break; + } + } + + final Relationship relationship; + if (ROUTE_TO_MATCHING_PROPERTY_NAME.getValue().equals(routeStrategy) && propertiesThatMatchedLine > 0) { + // Set relationship to null so that we do not append the line to each FlowFile again. #appendLine is called + // above within the loop, as the line may need to go to multiple different FlowFiles. + relationship = null; + } else if (ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES.getValue().equals(routeStrategy) && propertiesThatMatchedLine > 0) { + relationship = REL_MATCH; + } else if (ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH.getValue().equals(routeStrategy) && propertiesThatMatchedLine == propValueMap.size()) { + relationship = REL_MATCH; + } else { + relationship = REL_NO_MATCH; + } + + if (relationship != null) { + appendLine(session, flowFileMap, relationship, originalFlowFile, line, charset); + } + } + } + } + }); + + for (final Map.Entry entry : flowFileMap.entrySet()) { + logger.info("Created {} from {}; routing to relationship {}", new Object[] {entry.getValue(), originalFlowFile, entry.getKey()}); + FlowFile updatedFlowFile = session.putAttribute(entry.getValue(), ROUTE_ATTRIBUTE_KEY, entry.getKey().getName()); + session.getProvenanceReporter().route(updatedFlowFile, entry.getKey()); + session.transfer(updatedFlowFile, entry.getKey()); + } + + // now transfer the original flow file + FlowFile flowFile = originalFlowFile; + logger.info("Routing {} to {}", new Object[] {flowFile, REL_ORIGINAL}); + session.getProvenanceReporter().route(originalFlowFile, REL_ORIGINAL); + flowFile = session.putAttribute(flowFile, ROUTE_ATTRIBUTE_KEY, REL_ORIGINAL.getName()); + session.transfer(flowFile, REL_ORIGINAL); + } + + + private void appendLine(final ProcessSession session, final Map flowFileMap, + final Relationship relationship, final FlowFile original, final String line, final Charset charset) { + FlowFile flowFile = flowFileMap.get(relationship); + if (flowFile == null) { + flowFile = session.create(original); + } + + flowFile = session.append(flowFile, new OutputStreamCallback() { + @Override + public void process(final OutputStream out) throws IOException { + out.write(line.getBytes(charset)); + } + }); + + flowFileMap.put(relationship, flowFile); + } + + + protected static boolean lineMatches(final String line, final Object comparison, final String matchingStrategy, final boolean ignoreCase) { + switch (matchingStrategy) { + case startsWithValue: + return line.toLowerCase().startsWith(((String) comparison).toLowerCase()); + case endsWithValue: + return line.toLowerCase().endsWith(((String) comparison).toLowerCase()); + case containsValue: + return line.toLowerCase().contains(((String) comparison).toLowerCase()); + case equalsValue: + return line.equalsIgnoreCase((String) comparison); + case matchesRegularExpressionValue: + return ((Pattern) comparison).matcher(line).matches(); + case containsRegularExpressionValue: + return ((Pattern) comparison).matcher(line).find(); + } + + return false; + } +} \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/NLKBufferedReader.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/NLKBufferedReader.java index c52476195b..d2e56c5243 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/NLKBufferedReader.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/NLKBufferedReader.java @@ -95,7 +95,7 @@ public class NLKBufferedReader extends BufferedReader { for (i = nextChar; i < nChars; i++) { c = cb[i]; if ((c == '\n') || (c == '\r')) { - if (cb[i + 1] == '\n') { // windows case '\r\n' here verify the next character i+1 + if ((c == '\r') && (cb.length > i + 1) && cb[i + 1] == '\n') { // windows case '\r\n' here verify the next character i+1 i++; } eol = true; diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor index 60379edba7..c561e1a5ef 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/resources/META-INF/services/org.apache.nifi.processor.Processor @@ -61,6 +61,7 @@ org.apache.nifi.processors.standard.PutSFTP org.apache.nifi.processors.standard.PutSQL org.apache.nifi.processors.standard.PutSyslog org.apache.nifi.processors.standard.ReplaceText +org.apache.nifi.processors.standard.RouteText org.apache.nifi.processors.standard.ReplaceTextWithMapping org.apache.nifi.processors.standard.RouteOnAttribute org.apache.nifi.processors.standard.RouteOnContent 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 new file mode 100644 index 0000000000..432aaa7e8c --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestRouteText.java @@ -0,0 +1,590 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.nifi.processors.standard; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.apache.nifi.processor.Relationship; +import org.apache.nifi.util.MockFlowFile; +import org.apache.nifi.util.TestRunner; +import org.apache.nifi.util.TestRunners; +import org.junit.Test; + +public class TestRouteText { + + @Test + public void testRelationships() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES); + runner.setProperty("simple", "start"); + + runner.run(); + + Set relationshipSet = runner.getProcessor().getRelationships(); + Set expectedRelationships = new HashSet<>(Arrays.asList("matched", "unmatched")); + + assertEquals(expectedRelationships.size(), relationshipSet.size()); + for (Relationship relationship : relationshipSet) { + assertTrue(expectedRelationships.contains(relationship.getName())); + } + + + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHING_PROPERTY_NAME); + + relationshipSet = runner.getProcessor().getRelationships(); + expectedRelationships = new HashSet<>(Arrays.asList("simple", "unmatched")); + + assertEquals(expectedRelationships.size(), relationshipSet.size()); + for (Relationship relationship : relationshipSet) { + assertTrue(expectedRelationships.contains(relationship.getName())); + } + + runner.run(); + } + + @Test + public void testSeparationStrategyNotKnown() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + + runner.assertNotValid(); + } + + @Test + public void testNotText() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty("simple", "start"); + + Set relationshipSet = runner.getProcessor().getRelationships(); + Set expectedRelationships = new HashSet<>(Arrays.asList("simple", "unmatched")); + + assertEquals(expectedRelationships.size(), relationshipSet.size()); + for (Relationship relationship : relationshipSet) { + assertTrue(expectedRelationships.contains(relationship.getName())); + } + + runner.enqueue(Paths.get("src/test/resources/simple.jpg")); + runner.run(); + + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outOriginal = runner.getFlowFilesForRelationship("original").get(0); + outOriginal.assertContentEquals(Paths.get("src/test/resources/simple.jpg")); + } + + @Test + public void testInvalidRegex() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.MATCHES_REGULAR_EXPRESSION); + runner.setProperty("simple", "["); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + try { + runner.run(); + fail(); + } catch (AssertionError e) { + // Expect to catch error asserting 'simple' as invalid + } + + } + + @Test + public void testSimpleDefaultStarts() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty("simple", "start"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleDefaultEnd() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.ENDS_WITH); + runner.setProperty("simple", "end"); + + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testRouteLineToMultipleRelationships() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS); + runner.setProperty("t", "t"); + runner.setProperty("e", "e"); + runner.setProperty("z", "z"); + + final String originalText = "start middle end\nnot match"; + runner.enqueue(originalText.getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("t", 1); + runner.assertTransferCount("e", 1); + runner.assertTransferCount("z", 0); + runner.assertTransferCount("unmatched", 0); + runner.assertTransferCount("original", 1); + + runner.getFlowFilesForRelationship("t").get(0).assertContentEquals(originalText); + runner.getFlowFilesForRelationship("e").get(0).assertContentEquals("start middle end\n"); + runner.getFlowFilesForRelationship("z").isEmpty(); + runner.getFlowFilesForRelationship("original").get(0).assertContentEquals(originalText); + } + + @Test + public void testSimpleDefaultContains() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS); + runner.setProperty("simple", "middle"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleContainsIgnoreCase() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS); + runner.setProperty(RouteText.IGNORE_CASE, "true"); + runner.setProperty("simple", "miDDlE"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + + @Test + public void testSimpleDefaultEquals() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.EQUALS); + runner.setProperty("simple", "start middle end"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleDefaultMatchRegularExpression() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.MATCHES_REGULAR_EXPRESSION); + runner.setProperty("simple", ".*(mid).*"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleDefaultContainRegularExpression() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS_REGULAR_EXPRESSION); + runner.setProperty("simple", "(m.d)"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + /* ------------------------------------------------------ */ + + @Test + public void testSimpleAnyStarts() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES); + runner.setProperty("simple", "start"); + runner.setProperty("no", "no match"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAnyEnds() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.ENDS_WITH); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES); + runner.setProperty("simple", "end"); + runner.setProperty("no", "no match"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAnyEquals() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.EQUALS); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES); + runner.setProperty("simple", "start middle end"); + runner.setProperty("no", "no match"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAnyMatchRegularExpression() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.MATCHES_REGULAR_EXPRESSION); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES); + runner.setProperty("simple", ".*(m.d).*"); + runner.setProperty("no", "no match"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAnyContainRegularExpression() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS_REGULAR_EXPRESSION); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ANY_PROPERTY_MATCHES); + runner.setProperty("simple", "(m.d)"); + runner.setProperty("no", "no match"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + /* ------------------------------------------------------ */ + + @Test + public void testSimpleAllStarts() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH); + runner.setProperty("simple", "start middle"); + runner.setProperty("second", "star"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAllEnds() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.ENDS_WITH); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH); + runner.setProperty("simple", "middle end"); + runner.setProperty("second", "nd"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAllEquals() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.EQUALS); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH); + runner.setProperty("simple", "start middle end"); + runner.setProperty("second", "start middle end"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAllMatchRegularExpression() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.MATCHES_REGULAR_EXPRESSION); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH); + runner.setProperty("simple", ".*(m.d).*"); + runner.setProperty("second", ".*(t.*m).*"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testSimpleAllContainRegularExpression() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS_REGULAR_EXPRESSION); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHED_WHEN_ALL_PROPERTIES_MATCH); + runner.setProperty("simple", "(m.d)"); + runner.setProperty("second", "(t.*m)"); + + runner.enqueue("start middle end\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("matched", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("matched").get(0); + outMatched.assertContentEquals("start middle end\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testRouteOnPropertiesStartsWindowsNewLine() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty("simple", "start"); + + runner.enqueue("start middle end\r\nnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\r\n".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testRouteOnPropertiesStartsJustCarriageReturn() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty("simple", "start"); + + runner.enqueue("start middle end\rnot match".getBytes("UTF-8")); + runner.run(); + + runner.assertTransferCount("simple", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + final MockFlowFile outMatched = runner.getFlowFilesForRelationship("simple").get(0); + outMatched.assertContentEquals("start middle end\r".getBytes("UTF-8")); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + outUnmatched.assertContentEquals("not match".getBytes("UTF-8")); + } + + @Test + public void testJson() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.STARTS_WITH); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHING_PROPERTY_NAME); + runner.setProperty("greeting", "\"greeting\""); + runner.setProperty("address", "\"address\""); + + runner.enqueue(Paths.get("src/test/resources/TestJson/json-sample.json")); + runner.run(); + + runner.assertTransferCount("greeting", 1); + runner.assertTransferCount("address", 1); + runner.assertTransferCount("unmatched", 1); + runner.assertTransferCount("original", 1); + + // Verify text is trimmed + final MockFlowFile outGreeting = runner.getFlowFilesForRelationship("greeting").get(0); + String outGreetingString = new String(runner.getContentAsByteArray(outGreeting)); + assertEquals(7, countLines(outGreetingString)); + final MockFlowFile outAddress = runner.getFlowFilesForRelationship("address").get(0); + String outAddressString = new String(runner.getContentAsByteArray(outAddress)); + assertEquals(7, countLines(outAddressString)); + final MockFlowFile outUnmatched = runner.getFlowFilesForRelationship("unmatched").get(0); + String outUnmatchedString = new String(runner.getContentAsByteArray(outUnmatched)); + assertEquals(400, countLines(outUnmatchedString)); + + final MockFlowFile outOriginal = runner.getFlowFilesForRelationship("original").get(0); + outOriginal.assertContentEquals(Paths.get("src/test/resources/TestJson/json-sample.json")); + + } + + + @Test + public void testXml() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new RouteText()); + runner.setProperty(RouteText.MATCH_STRATEGY, RouteText.CONTAINS); + runner.setProperty(RouteText.ROUTE_STRATEGY, RouteText.ROUTE_TO_MATCHING_PROPERTY_NAME); + runner.setProperty("NodeType", "name=\"NodeType\""); + runner.setProperty("element", "U<$uTqMTyxDe=bZI-mHaXN z$8+_~m^5lM2;}DnLV`dbWsr)DI!G3v6oI`#X3L*66rkBMTS0QbSqAul{x@0|r0{1s z2y{(G?;rZM4E*oYl}wB|e!_@oV`Bx>68>HD?=$`+H6k{M8Xsk3XK7_=4YIbfvUWtF z98va0*4B<_J4Y)AkY9L&Q2@~>)CdLC0P28r&NBbaxvbUM`hRHq5)eoUXkXsS8f8@? z_ixT+T}$Nup@T|g{;DG*D+el-{pZ=3G8qu~FWzJm%l=cIRwn;Xc~+UiKlRU-Dg7-0 zK%Qlr{-yz$m4W}MQ&RTB#=B&G27zR&%kuuDl>zohjsNBoWo6YU^B+kz%Kk+wx`IHB za(~hCS3n?^+P~XWILuQ2hmK}#`TJQwYpiYmp_~6|UuMGu{@wsP`13n+}~H<{$2dv&OUx#j{m#o7-0i3UjNkcq$bCo3QvwQ zB1I*qP>&}Vp)9R7Uf~yh;)Ic3__5>B(bR;0YX65Y1ilnta^v^+cH_5yQ~&4uj|8lw z8{~b^j!PToT96XpNPk6BK>srTL{Npn|3pxQ(f=AjWo1C}z>|P&C``-B{+r%p{$Hc! zM$`X28vZf;$1~6ttf!ABNJd5m-+oo$^W~8fcs=sZU@h)T2op2-)sk7Y@Wr0AM zA&_txC1qvsCa~t_&6;q%ZF=zk&)Xk&Wmv%HDF94v@cH)!L{xbNMJ44;;LU(@{dMo3 z+yCVzkfvnhpld|dl&RbBz*oPf9 zl;pFUz*^h1b#%>OJK+c<+SYE5y@R8>2gcLO+XqVw3MPew9w0|VMnxZsq0&-LoJ>8H zmY$JwE;ld#`~`YhIisSos=9{BYG!k|ycT|ISGQ2qbG5hc+U+}c#r*?=L&M_}(#ffZ z(~oAJ&o8`qx%le!(wmQ;KCiB=|N7Qs~cV^ zJW12UI$IgCzqG0I)+SRM!UrfNc?_(z1O43W;|AA%GX4LK=d=I6nEr?7e^~zGCP+gW zlm_Gx855AKhK!ts%pWr#6*(E;LQVs;5A=1~%pR5*)TYDN+D)q*sT()eApZ0Vt*evy zV|nR#(}-je@zrCU{pn-K4`pfn2-O>wiNU2F9Gmz`h{lvI|-DbX=ff7ngS8B6@m) zF4CRvi+tditXoRqg{Js>%+cOzEcC?ccrVm@(tkX**syD8h3s?udKq#gwPpZXTO&cP zTSay)aEO(o#gkJmRicopfWhxSO?o!#qv8{ij~_3d)SzEO2tbS!KfG( znSLVuWMh2i4D;yT3ZBE&tMIQU@@ztWV!Ce=>A%dRWEW1TL<@hue;UD6N#(78gU6T= zdl};u?C=FnNSSkLE3uCq3nN5{t)%y@XfT25gxx~F!hr%sesj>rt0sLrE;`1HW_!kZ zD(I8K2;dtkkIfsr6oMq7&4X=~9~YptsyClS3s>0kB(F>8u^-=N-;C1N4Y-mtC@^pt zEy}N(9E%^)mSB(VsP%G!Rq7{Zkxt()dK!PKGt|7p&p}vGTC7N$FeW8A_rzONeL*%) zmz-J2sCbxv;L~i%7ZPS5e_Fkd#6IDrbzknS*Le9`hm}KxOyli${B)guOzn6(v7B$Z zZo1@Wr(|H>bx8ZtbDrJ#q+esMYxYk+Zy5X*yt$T=_3*S^agb(D-|ysm_lRObIqpqU z;bNJW0rKc%G@XX$9M)eozeQOk_Hw7xq&G0A@LRAr zmVa}yfgAMmwhQ-Np|WY{!RfHfkB1oGu81esd@c6ES%i=spAU^So$OmW`ax5S9eSNe zGL*}@Z1ofGVPji&n(X89f4TJh0lF6Z3rHr*3N8p%7CboPV7R?|y0t56c#8 z=KAHG9}nf1jP#06A3qsuFsH6UR}L?(H(&hmeC034qzNYdV3cAfUoE6*e*VgCxx<#0 zACFx;JdkHw1-~VbxFz;?a^J*~L?9sV94PlaNEA6tvLZ9Eh+wr-`~%9co&)1PArHiq6J* z3nwokZ`eE%#s?pZl>dCVbd%x^(m$mqfeYHZ``Kn_;bikK^FsOmyyDb}g_SZKBdyF}Qy&P6{;9|OrF%)5ZR9cq`uz`(Ky94d;KZ|kz*HMqfBz-4Kvt_<1@t<$@}s)= zdsR_Wx4E*j)?v{L$BP%9;8=Zf%VEELYulSV@L6E`AofrL+pM@v51TL$()NXw$r}08 zttl<9d#KlPq-N7lYxk4N2ez0O)`p^X#PrIYqYT>!Sb5TpNe4aK9S`m!uj?+;UA_gr z^gy%9vdMbI5;&JmmbWL~;(`;|CKx`ciCmG!k51lYcHlxQQ-r%nzwP~^E`fUcOIzpM z!i|Vsz3&6Q`#VipZnNikRX_gMm@+$|eO7Rw*2p&MUTT0&s{rBhrrD!4%`CI)^W(}l zG^O61H-fE+dlKwBa^65vJMTj9M}KuZfnHfSgJVeOMZ5=)sg@SJd*PN( zU%6WBHMVVZ<^FdMuo=IJww~nc2~>`br$RN*g7Mn9K&MSiEEyZI?~h0S2NwMwSoHsl z<^Bg)BpWJc^gqI)5ip8QhVOuL#GpV~N)<$SqEWs-Mr@C#gAx$bOu~GLs#qKS%$}8{ zR~#dDf;0xoCHc!%K^noJ1O*WBiUKIb+tQAn3Kz>H^)^G^-~zX4LaKQb`cX9AUo1ma z0a{6<#>faM^>mbLKLw~c0J&>@mO9Ng$1t#7?esn2#5E@fXvJs>7aOR4l7Fq zPlq?kiy@78S7o@^LNRM=A)!Gq|JxlX3`X%xh``V9DYo@Wwz{!WbPZ zZ(-2eJVRH6L>CJKWykRt5JW$E(=Ia!+Suf@i5UCS4IX5|paTg)IJW3%xIbjb( z*vsjsD)cjP#7;Me0w9``+PuWx8M}$~@s(B90xlXZ-lx6nLl3chod6d*X|u9=hfx7y zGppVa!hC%p;Z&pId?B&On7d6IW9C@$*)=R4ve9JLTp%LJt&yUOJ!uRGY`yU`A_jdE z>$b{^KvPQ2p#do=nr+A!5a}3hKc0R#rcwSk1VWmZr3pF7kz+cJqSzT@06P-U9tfr0 z2T%?n!i6dgG)DPcq@9GQ;0Q_nVi+96zQqf~fb0MRl0hJ**E4niQ3Q!pT7 zG~*2xgheEY&E&Sy!|;IcAb~PIfMg+nRseA_K!1r@cs2_RN1uU!2zVx3cC--;@K5*1 zY6O$R2+)3}D`J`>MujCHl9-8*Ubgv#9tJopP!R#1m!Sg%Fa|h8lR`Nwe2d4vn8PY_ zZgIrs^aYf}a^}oT;7|C(MUPMbjblJyOLRY_PLrOBNRpW6_A_|#H#v}}WRJQ1GTiYs zwwP@2*38Tvj7xFD?k5ld1dsZx>P#SZGVLaMzvcx}?-D54fF=PIz-(~TBH2Q~U4RlS zj>v~=Gpc!bdW0Tb)e>$xm95I*tavK`-&u@^k=e@-Qi>TWxFlBy;33NKka>IdlItcq zhyxfhLl+Pp%4fXZb3=yGK)D#Ph4zNw162Xr90P=A0Z{?;?^sT!=zYPD*Za8MWmOgi z%ARCzsIaV_js+o5zFrxClKy0bFuvgkKCTK7VO_?k3?Oq^JjMc$H@7Tn8-zg^m8Af# zwfTgQQpvtQ4?Zky3{;TqC1ktGR%x?+kAgU2xDL9PA6Hu>hAW{PiCK+)=Mflf9)(@` z&IoXh-o{d(jy6xm+ZEKf$`QjH*s#1(ANm;^H%NbVbu-{K?~J)yQV5hH;ikc81d5Q$ zQ~tu5U+h$b&pzRr`^`$Wsl_W&%Hw^D5#X_;xdI4*WJi4kuz=-2R}r$f#W`$F{SI6wiyn}E?bL)7Leg-O0%wD2U9 zk1rje_8bF9QE{A(!p6v;=-5;Q$~6qIM6V5d^aGp%La8jv0t1>u;biG#5Fl_fOE?<{ zBNmXGSOwX$E-f;SqtqlXG6-m%m9=He6V64cY6G`HPh!N5Wm#M3fXuO={RoslVE1Uc z6=45p5F=lnzy!kFnAj372AOF$LxiBbG9bD+j$r_;a+*J|8|9dAR@UzMEC(K8zPm5C zf4-g`2xt3JC{@QH!bVEoTfcWk<4adaLSYr)fbC=Y7ZmFhEbI~J z>ab|UP2yQ*AmiK%G<@09o{uV?8+DY<6;oV2Py)%FtUWzJus+fCe{6HNQ5D8fpkVB2<-mJ2_ZcKXH= zhdM_ssaWHd5~17*UUCPfr$pdOr;^($*&Y}e;w2R&M&Bd!ZGY}VFDa%f12wGX%Ptj; zQ|bqjgw?$Ha>%>j{`rjzu}uagh9RcWl!Dy;k6zd?Fv`^!wS?I{UYO!~cjFF_WFCs- ziJgEi^a7Sflu=duE4tIk8~U@NCV9d)pI6Z4H0zJbC3$Xy9GN6#R%JP$e;|~|_09#l z@+jqyn*p~==o`T=3-7z(S12In*i}|R82x4jOEJ!Xll3E#FtVB=qa}ZKBZUxweuIn7 z&c`MorkepZu}vzgenEp~5HWxbA;RiL`FT3o##PsT9%v(Crqdxo8{jsbqfdZ6Q*hG9 zU!(;hHs$*>dI6;Z;Zq)F&lwO|Dx-TBOD!%GuQosNW zrwjFVw%eX9D&rLPcC6i463hn34J-3i@#e(~c7zqNvz({XICOCRqpS7`Azq?w&r7oN z^wK9p=m%9d8`h2Yem%dwAhH}I$P^1BW(vxaU4*si{M7M^`GT`GYGZy#WIS)YocfA> zO1E?qIsC}|c8C})aH`~nn0u3Wh+^q&kq?9IN+^%Q0Yeq7i*yesDmzF_?d2s;m;w&T zF=3KwupW^qVi?lzXX(+grbBvltN@7#vkSOvukNxtJ7;WO+w8Rbh4Blg1V z$~jIDe9|0sv5>IZtOQX2Ou_*Os!B=1+7y2=kRiu_(2c^K(gfVUQv!o;F8XtD1tlcR z7Y3sCG8k_G)B!^vVg@n*uLV-iMr8QFl}%-Hnwc9Jj}5rFVrBUa5Db7jW*|sDgiSaJ zsDBItqUnS2Qza?>(CmH?o}P#nUg*urAY@||KpBKBG6AxM#4Jwojyw8eS4EaU9xBrt z^#eZe!%u{T3BfklJUC1ja8wD*?@l}@1DyDcP>k3*olTvk-mZc=f)`w6fs94ScGoPS zl*jPx7R&Xev`Xve^%uL4@rm%F`AXYank2b zW8ILjd}}#FmnCp!a1pCSuuZ|FXNKyZ4vpm49BsAme;FM%aR511NYt_CB{S$8doFaa zZ5N{-%a#NU&mzFzgI-cOG)7U-pf`J#u);PEPL`fe9f$JI&ofE&!vqu4nc5zXcu%}_ z-}#T8h%e2M+Y2Zk`f0rwK+zBlu~WP;M7??g@By$VVsUFBv1jZHJ}E3=1_h{|^?YOl zC(uz|Enm;E#rAZR7|5^lXMbiqvu}pHb1IzOXflSA2mlu#)B~xQZ6ng=#iOrN=T4ap z0OWj0w*Nx!I6}cG0dBHk96lQ+1)!l3LgqYM44aAQ9mWv{oAoDp-&uhX)9W0u91#CE zBE{xH)A4f1H>~VJ;s!p=JDUeYANqd8oFavO2B8WVU=(l-h7rlujK))(wBlH_Q=d|gH{4G zvK*imHGNy+7dWtSzfdDJ1uJ^k?LM_9)I=A=cWUZHZ-4G9^hz@`b#9d>&mPj=*2Gib z2<(fnoG4>*=gSFfitE>okXbj^%%JZG@MIXxyM0J()v`na zM>7LYagU%vFN;uR0?3LViqiU$AIc4xVTLff#y;aBTl?ChNi#NLr@Uk~>?TQ43ct$97edk3-tpL~w@LO5N z#D?aI27sQz3L3yr;bgmsuV_kEpWzeWjpqoIf{WGBK-;r!+KrG}Q!$#(C`Q;#hW&5{ z1(;GJI7E%&!k}>E=-|(off4tfs(9avWR(B27w&XB>2)f2Y{b@Ns|z1R)Bx<|SGbcmoTwq{cEs(-dMXbuQ>P>VvAdPG-1Hgf8QUHO9 zTJgt+z1q@9$o@*02XLm@*)^UzS!ujZN#q-82O{2N%h5CF<#u=ViDH-wg65;eAi8o` zFRqAppJ6mcHz-o=d?Q?WxtvV_n~Nv+n9M8i@rl$%;zNVhgT`e!^gX&qhtMC?X&xvw zKbdqMg#kX~^D|R*(~AG56+$aL>2OS+^GJR)B30UyNjP_lla!(Yrp{W?T&n>?iGFcS zqqTgh{z_0b^=6=NpV|W&72AEam~lJDp(!*Zrh{0F;Mlh*q_a`$JSh!BVbt1?z!8N6 z`CH9|zVpsyj^J3cu(z~r1ZolX!T{JeeSQ?^6|g9~XZ(vWzL1;b)^O@Vyw z+cubu->1(&;21j?O;tI0l)m|3?Fot|zMv`N)&e0B6h5IF)Ne1nZD zl4v)MVo(=+e8)>hQOYbjTrP|_#YD{a)!c*w5E+5-jYPTfz;4B>&lG(0iXn{=9J@Ni z*4+;CRX{Ij=*kVmFhUVAsOoDxmpYRM45cU9Ka-Y*4mh(ewO6T;(!** zZQ^Jh3eMm_n*iC;`n$9Knk9tD6(Cf8wl^UU!0@DvQ`UYaTzeXp~z5` zfF>dtY;zL$I}sdwNEZQJ2NF^{bdgDDeTgWEpDz*MNVVO}Qx@I@@h}H|5561nZZBnv zFrJ_K*{7!<5eq?s+$!r^;8`fZQF)2 z1TtDx0u?;p&`dA|;?)YeHfBTfcHfD&>8W$$ah6_J(eZrQ1rFi}*3yz<(IBn$ryc5j z1q3GC`P~8-GK>)4=y3ivkT-#*PMKFn&)@(k22ny80uw)|C*@Q$G}&YymBBE{v(a(9p&&FfV4PzdqgdVY# zNsW1rQyla4ah06pV?14!-k4X^ndVwRxNJ$*i;2&Ea%f?VZt;DpCl&N79|Eq=!u-ac zHmPKQYjg9Y1wKHRxUeo?4Kb_^8AZCH>sJvG?Xtv4=OxYMM8O_p)LqV?lB2@1xN!3UsO8Kd#ykDHW+}iJNVNdy zG8GhgXPN1x^LxhrG{hMo^TfZe%AQ>TZkQMm;`u#M+t; zb(_EDV`+p+d z>WEI91~4m8=Dly7M&MvXK7h?fsnpQY+w8*?TU_2?oX3ym7uO>^sWLp3piNhlME3eg zejI}sROGz?a*-l3c#-9aOAK4*We;-mcmki=9eCy$C!&J0E>1F?rVUv$WQ<9?)f|I* zj7VLvGb7XPQ(==B^+e3j*;posc zN+Z7nd|oA}t+beNF7ybEg!_aiq;dT9Ou}0U z+BYg`yFKR-v(C%&{+jCx_wHgwuA)6O;wbx(|L^j_v&GW4LH*8SFUL1`@p0>1bb4E! z%`_3W6D`s$gIC|+b9d85KY1nj#dJ9);eO~(FPLeLxq*rK8oGiX(u$E%S8WW9{5&HB zigXq#SQnwQV(1hUZL<_IW4ZbOS-?avR8Hz#oVtt-lg4j%1_MEjcXft&Z-q2+;=yk8 zyCbMTAPNg3*r;z4=5_5`2yv@zhDY!|UD= zqp8Vt6>uTFCf74vLMuMseL?h-(}d(P#)&$u zjzqqFGjs8n_8R(G3J&rti7=+N(2Z5yBY-c6Omq!Zy}2x2TToZ3nG)o=#Dj|r%iw&Pg-r9uP zwh3s-bqRp(*lL00X}7rXgI>&6)TnC8`71UnP1UI?#&11Zy0m{h!DY#9@vM)Nv|x}6 zF3H3njZ1`lP(-jAlwP@e#;+OHNDf}?p|zf+Nqv_C@A_TQxXJ$XduvRUEeiLU?itVM ziQqUL6XEU^ORL*(Rhe(xuR@wU9fPVH)iQ{%yH0S#3Q>c3s3Jr|y=nk!2Xyin*IoDu z!~Z%0$#rR+#s=(0D?FyNc8#MCT?_8qCfWx~o&&SfHibLXDm2~6yA6?c1upy|yQ?;2 z-J-^4AIYBTc&v$>NKb9i>(S>K=K2?C7X`GvvTZo)BaUOL^IW`tEQQ?Vz7mLrgihil zF`YBjlvkV>A^HLQSQF5L@VK*rukKqS`)w0>eAEX2;2XQ>37J2^xP2PPx&yu3rAieH_Ol*)o-=0Bv zWe#A6N6VztOd#Bs^bOCdW#C^Uh zcG1pC+N&y`m)`>iR=`m%t$VOPMcB6-fcGWU_>{3u*q$t>XE(RuUKi5+MTf+PO08(Z z0y6jHehjIlTKCF)QZcb&-Rf7?hyt%|Yl1Z(gY%m!L}pM%p0| z>i7ip@p9b@eFSFDUMZ_|M-4}osV;)s?$|7L$e2i3!K%XCJIka?A{-hxP5?^AE7?`ApF^f5xR`y@Og!&Xbe34p(^EoJGEkL(%hJ zV9q~Q;&^z84w{$Z(I>`s=$+>8&+#ANI0Q|dJBY7E1#I#Mh7!%s_D)_zKdUh5P_?(D zvJQPKeLKkc^m{i+%Z8ptuUONJ!t1#3anT*SewdiiFNTjEvW!Njt>9w1LkfeQ?nrGt z2ws=6!zG3RqQe^CBjz*NycCD#n|mt`&F`(~7>#bqJmVM`vd7)H@qx%=^p!KIZT5Rm zyo>?=v#O0*<0@1mzoDXiG&9IkowgOdXrtm;J^BqSwYr48&?q705Ugi*)57zFg@)2Q z>e^x}LUd9-lUgu-xf^vAdx7!`M<+c$d6LabhOhTL1{XfUt7_)a_* zx`Mfo5@lO}e!g9F5csGa!J(I82q;R`y}>rFN<$q%yKf0o;?Q-WV1_xC8N8rHb-wK8 znSDRd>KjodxN-D5k-VEmLf{%H>)h(ruk>oldeg>fn3>nijO{Slj^_$S{LW6_cYs zMtTVVn?_o@w0R`tX?SDo$91EcdbtzV#!n+EW1_3jbK?&khDSe8QE|vY|TU z8O95%hS=)EJnTf=qyHJJ>R0&4gqhYdIw4&w-yc%~*lBvRtxBa31}sU~FCZMFlWeGE zdFVA07SUgkQzOK&=NoJS`6~f)NrXYsg^KHcSvI+tkb$X|-s6*kj4nZlQExO#4QBUS7w(=L-jSXHe@l_bQrRpXuMK(FaI+h=$ED7}z z2YK>?a{~yv78mU21@4Ged;~Pg3gy$J3qNH^J&@MY6bHVd94_{}D|8)3C}78a-v>Tw z;3XhO$3a#1QN`HC2uQx1$l6i4jp*6Oz1+xW7}_^h4prDM`Sq*XZx`qcHOa*}QZ7kP z{sspl65*ru-q`4JAh2sOc5{NimprAQ40bb4BX92%U`-#4XNPf(2bcV;G-rfBD69zA zx$j6&i;mKZTZ2L>r&kVM+**2jzRelbz<3AtGzSbD_N0gT)Sxr4+NHJJ!ndhx{r6T_ zz8ojc_7~Cqsozd;>xWaK-Q61|HTTCJ61>s6`ic`MExc^P99s+p7qjerX8?Pl=@m!idd4#naf|JD!3L{FbDqcv5(Zcdc|o4hY)uN8B-oUh#FAzqz23`@snv|6 z)CmF;czGNOd8c}SYVc$qUoM(;H?)Rax1E`fem2b>t1VjHr3gHIC4u_<>oQ@i_JaKx z>ho66>W4>=c3+QMee+yZaWH9>FGq;9e|^B6kP}?7^!zhnLA5%csCEd_=&p;fZqUI3OV z>d7Lrj*}Z4P^15XjJ!J-QOtv7rHnVKUlh2InOEwk?mqk1iI0IziyXzXdWLPx(jY?w ze@%>QrYj#U$RQ%dEA0@~^YZM^=#wqMeVrXX*z=3SpvGoc&V`lcTmR zt1X7J4u62a9pRs~En@M2gCvN45xK5ETJdwLkVJ-yesMwMJ?WWwc=7raD>H!BnG!Z& zl}V{SDu|!~iymiZS{De`g?pvx_5z#u6+gQ#3y_YTmW#(reP2X~sHZQ@WG7_Ax9+UI zzh#l(_7uwPIk(Plz7lOe>z=Av%CPu6!#+sJB&|BS?+I@E@Vf5wOg=jMP+jYz9!HIU zgD@AF-`ctk>40v5r1v;?LE0_BBE1~@oXEK(KRdtQpWeS-&cPIGHJ&z8|1g=-XG zW+niTplat5{q0k)!;QfX!ej@zPtdTtDf)cC%TbN%g0CkQk34UE5PMZbmj3X2At?u{ z=_ZMDxhit~kQq2S>2Cb|mN>W)`KU}CAGDZnz-MqNlI=Wbz2iy2(1M&Pv?9rHQ%l?m z$5iX&En81KIpy-s@$Q%8Sa&IV*xDi_oPKrZc$>Wd|JtphItYgZRv|%Ij;z7+ErYh^ z;G$geo&r-7CUvozHg-+t3qkiJdQulgp0BK7k_Y|3r;A{x5jLLG=>m1d`nsTvMFV$k zN$hnC4dIUoz8=^4r=nTFB?Z-AA}dyqJkJoS&xoLLu?aWFqxe z>#peep*!R6THFbMKc)Lrv|?zNdx;gTWK%|$0C29H=my@Cio!Wx<6LK3l6uY%e~(n? zUa(7%rvwe#2Wff}=G{pR1J~?mb^6L{Z&26)VGe5 z`%@0*C+e zNJ$jzB?E0sfLasW_`e3VNH!m2kj)e*a{iGx{SF1^h0_jz|=7BUXseETPwa@ zY_X;45%}BvV{&-3+=OjPJZxsyUZRVj`w!4mi{!E`Pnbc7TjzD!Mf;LJPB6oT18$Po zuMy)H;Tgnkg{JdU`MoX72Qya-jXu(;JxU_NHR$7!?Oofk$EUw89sC0nbp*S2_-DrZ zq(jZ}kII10q`tgPdP9~QO(K;|=+Bq;vBMqS79KpkbEgH_iu(G}9r9*n1O9i^Xi##` zNq6=5;pK$3QE-W;3o=Q^r4=JeeQh2ZUMpP+`i6m~s$-ph&LEu)_09}h<{8fS6zldc zZzh~pX#%i(dBbGaF4RoPqfAmO9rKf_FRk#$ZfWwt@1!x|0PVtx&q~PKv2!2%XZ3%p zq}}|D)e=U{dL_5o!Eb-SaWlKH+Fq~L^1J~Y8=0gz-F56`tM$PMQR+iG=k1l99qm00 z=1OTd{5RbWOf4PpJwAYKPLk1?zlK=(IMemcxIc||+LN7f6}15C>?jwUIE7zufaa&x z$0YSV^p94(eRS%?ZP7a=@=qpGyhs72qee z_TO#}CoqfhZM|Ou4pmKa$Jw$RO$LEUl71#lqG7dokru;g*`;b`>^ycUgSE?9H}yTX zP&Z5}G~b#a)@cEdmpS;(XJ6(~dyWJK%g^yG#YuD)E6$A>#ZBC5apOf-Te-2Q^TzF|=4z7q=35MQ$9}pt(rQ0O! zZk}v;v2-A5*sTq9uVIZbR-1iUGl`h5-98vCCLh31-W{F4YVl5tEXxd%=_VMnUL;q_ zZK2)v8&yMc;BmUt(g>1{4>ZHH7d&t_&}oX(R!F~3-Z~B|vvvgM?>H&2$S&(%^lMM@ z@v*V1&i?qMuIt%?ZCD*)6G}`wAIop-ju_|qbRQ@%!T7628C(26?bPkasN+JoJ1)l8Vq~?T%iVA%Z%^`_S>a@^}h0%hK(h8G%1{nd){bnBXOoVHff2}B6g}7E&yQ+z^FRG+7!v(M+;e1d(lN7)bDH9#PvDE!4zBjc z*55encVNu7FXzNh4d&KI&B-qeW39s<(rZK4V!9piOTm%aZC!M}k%Zvl7}ql2M7y#8 zmCGiZ_Uy{d^_Vo5#v#YXw%S2;fVsLmR`h@7hy74V<7wcU3419QK+FofNH?O!6DB zrRj8KOxc$+8xVcV2e69%if@lI>+~sI&*~yiwJpJ1J5F(5q02oMaY^{EeM`6$e2!n7 zc}&c~p$9i&$-adogFc6Kf%idBgvdicaX9Iy95vWajXJ-_q&vL#mj2XPe`WotZJpM| z9Gxm^>v5Pm`FiT()8?>{#cM|I3TH;Ux-zBrETY>PYm6yw0*vK5xY*8S>kbu%1MrWY zv1Os@vi{fiBtk+(aXE$Ob1?$jPqHH(dAWvxr<_7 z&4W^JZrH7j)Q@!H?)eE_cAPX?p2;p5zDzy(-6zJZ) zf`O&6vh>p%3CAFP5b*6WPpXY;esmSA<`V6&FgN_!cn?}5=Va5{b)(f(b*~%^UOisf9e;n`++TzFs@!^ie$*@5gqdfnJ8gqM z`W5wv%%Wih57qlJro!$n3I?C&5~?TfO70elkVEuyJr0oCwiD3{b3GSFrOS4x1FygE zNde@mlwH(ayd~m}-@6aA!7>by!R4GCxYCxcMZhHWrzpN(gYH=zI9E3Ju=q^fWbZrF zlg@}LgEu27nPbCIW~Vi7s1CT$e%oWXc3+&B?9lcxEM=%A$vWnBc<--mog-@I4fJRF zDIw2$?<3Gr5l4X$vF_)Wo(N1ATdjMtYN&o#RT7C`@-WISUPqHJ@kT;v75=duc8-*L z8pW}mj~s)QGw2gN1CTxZm1m8$yFGiMX0{o`){9(v7hs5I&jPC`W&ej=KD;#S()pDS zzB1B~gP}hg<)7?4j_vRIN$16)^mK>rVXucylF#%7WmR0bH}*5Fiu#$#Eavz$cS zOM^jUmM-XUw{u+h=Sb&Y10*wv%j}Lc;y@TGfeDH={^$uY3YCHPsT+lFc|(r3kTaMm zD;y_m>Em$6M~$V?<>-@*?w_A9bYq%1FcVj53QnK!>Jz_n*O@ng)EWS4oTn{x`!W+^ zB<7motI@Sm0)5k!XXzX+JurQ-)bv&}dSoJ-6ONj%2bP09#$34aFX1zqvA;II({6{V zKU5ptXS=KNLN>20Q@Syp86#m04;8OPZ`vC!2_(G|1n9ibmtx`fy_e!RgZfi*me*q| zI;&+J1k}_eJ#GA)X>$ zVixeAqcx79CARhs?*nrtrs|pFFFsXjyg6CMZ%ZMxtv5|e`o+^PY#px(@SgMB1DoE& z^p)q{8-gLzUc6%7->O0Md{s;vdAZ#*7}k%J5?9+9<;+ffZh?-EO@cZ77gh05uid!G zY0eU`NMUT$ecz2EXBUb^58U?vL`G#H2XbPQk-e?8OVllUQ1IOnjVWQ_Oe6w*ID}u? zTILd<_b`lRfPR(YDOYAr9t(h8jK2qdEIOiW|2F6=Nv2sS4!ZNXz8DjQ49 z|EJyDCd}x-4eHds^9Qx^$2;3s{JJmduH;{8dU0)9?@6ED{k5s(15ch*3_XdA?xp^) zXP@J>MU-XcRM;dx_oQE)T53KABb+856%oqLWeA7wBfBRrAg_~J-e+$H=e9<- zFa-v96LddMGGD;6tS5O+xX^K!)b(q4u=xA58cPbd)KN%gu{n_ zNBX=pRJE|KQ-f=|zrK&K@xpq9rcyCzVz*d+I&31FtA@xJhuzJZXhr!$ccA7QRILbf zT$vlaH1t`J(R_m*v(nu&S6FS^eLST^F}Aj7(Duxe1^JY)3geVb1N@2ovz^3u?QOn= zzY0L#^PzVqymlk@{-!cXJ-Fku`_BrbvcuH=7os!G_q|)r-&*4}%^`G>$mU(CruNQ( z3dZwUv2LMh60CrJ-Vo@ulJerUG$c5+^j7*bzarO3gL0ErYS@;;OumE_Mow{FKWT6N zRdTHPGYQ}NF5E0ShFw8J#5(S2L;Ye>$`IL_lxr_uc0(;N2a5eMVf4e*oTvz`qQPT} zLgQMOWw6dZ$LlJ+Hy=1Ga{GZKJX9ClQp+67BlTxYowGis$V`Q)jZBMvtPXLHPG z4@N)v9eXcIzFO~h((%rjk2lO2Ke@!_)&3fAznxJSTl!glwVgk(W2*k%N!u!i`?N<- zJ(_F@l^wYB*5p-Q-OLQ6)ScRhqWd*=?T*VNu!bX(c#FiL?5maI0sBsDw z0XK-*ks6~|a*_kd3j2&Pf(W-dK?sB#VmONWh;0A|5Sqah1DKAMnCR-O0=RCR?lGN8 z^+JzfRe{+<#2tYZkR)R&^K$}PfG%?bjSN`Tvx)2#d>&5@)8XsUI?Zn`r?&Pj1t>P% zy6%m59WoS_j_dx5s@a=&+Egqw>OFjU6gT=jaAMtKVr8rf#?EkjA%odA9DIMiO8aQ-7kerq<&*|JlL~ z*Bdm~Ij}IwqCk0Wtxb8s*3W01U-D{3xN1eMDjjM=-3TlK2E4lUjy);ph829gdgI`Q zj|T?VqW9QC3L_=noT0W-UrD5Rr)}~cc9bbaCs{_|bRZp?t5PioZH~87UhHNXy^b$V zI;2($vrUw23DTqxG7RSPa6HDJmtwD6--i*E=whGwSM}u2`jj)VLdHdWlW)6ry ztlR%Xru#Dk_C0|5_B=hF?DWtxBSdrl`xWhuG}+#y36aw&=ePY~z3GOh+V&pm>mYx; zHuWza%xAAia(Hh_C~;=s=FF$QisYLwNRg8n&p%Ifb7`lft8iA`jW>1YqT7cvCLL7C zF4&mM#*1w?0*u8WDdgs#l`pi~@j6?Wz~FA+l&uc>6U~NHe7*@-mE~`WrnFB&UpJ$J z^*|hUvqPf-yZHtL#Jb3V1R7&3c4BS3mB9OBBpd$*f`Ej=nZpc_5RWmza3p(}Kwdm7 zoMV#ZE$oR-geBqk>hNMuNljgdQ+OtAueO@qH%gq6K}WWU_klo$w8`a>aRPF zYLZc5iUzSV+o92jk73>1lws!FL#m7mb!g{}K%Z>BN7hsrH>_iQcb;T>Zb)%>p(dg2 z*bQHjbyD|sSGELU8zY%ZbEY(?dIDCylM;v3gBrOY%@&wx_N{UGTaSA4+CVg0qhC1& zRDPJa?WBe2YMaK5KvJ8=UcUnczWm0E%&v>#WBspeqhZZ_Mip_t-4bcxOhw&A68?*l zL+kavb5%sbPJ!}{esjZ}FTlTS{*|+Xazd;7n_+z{L8nh%_mv=WQM7Wj{&)+06Ch`K`N(r-Q;Ek#X5y=%ZCG0xJd2<8Xc)Hm_L zf#+V5=r1jzxAT!lMts7VuiicFF!%g)G(|!Q8>)NukUaXenzja?b6D(tNfSApZ2x)$ zZCDR4!9EcY+=+uUcAn6S(_AwA8AUGX8KRuy6Q7w&}x?rH;?!#pgJyj3) zkLh_+B{y59Cu}dMLKwl!0#`Qh&ola7AwvifW{%MnKo*d>ai%;Rox%kEQ9@TWgJc0q zYZSA#G0C{`&o~7MctF9@-7GirpFF968(vUp_o4X>Sf~BycW}-+y3S{9z(1dNzPQe*}#qHJYL&(X|L%X z&BG3-PPLimybtZfxe2sk@+!<>{7V!gQ|qH|PxW?}H?D5&?^1+f{h%DhYQ(cM!69ax z@cByvv;t<@zFb18b;PZ;WkUaS>my!s2I)fe(6-VW$dP1HhC>_tcZasl0utKv#|)9) z)QR{f0?)aLE{B($98Kmg@|OgWw|@(suH}jR`>j3iZq*?7Ty^q0iZN>YFqmxaaC@H6 zIY~ZrO#dM!{De1%xe%vkJ9=Q~D)fQBf8FJj$M-t761p8*o+k1p|5q7T8r9UfwF3y2 zFlhAvf?x#!6Eq;?But_~3}l4lFbFB73N$8xsz^b_+g^}Cjerm$!(lKcCm~E$G_+tZ z)(NRYl$->o3ax0dfJ#-Uz1n;4_tNjjch~y^xPkRjB zD1b)qoNcH1M#xD${QnQpvu@N@_FA8r-@J5~ux_Q}yL(Z&TS;9%T<$6QB6-qyu=$4` zWy6lS(}`)XK62DQS=H3-lyLR)z{eK`a>jTU;&X<&kkfuM*ujq?j~QDA9~SwHw|BbR z`yF*ZEq*<7U?ZMB_Ef>oew03WUTPM`-5D6T@w|HStA9+MZEv9YU44X}f}45*cgHWH zvXioR7oj_iNwou=thuSH)iKH0G~X)YwnAfW?brvk=aSaTV#6m+Wr@7yl+PEHUB`H-Ou zY}D0A>wF;6>XhkP^J5R#c9xa|S;Fn{wMWF#lmQDrD7K4C6}V3_xJ{ z=;!Wp3)Bg~Vn`|TXx63nqyz0X0@iQRQLvP<6JEN}^9}cp#K^>wj_lqd^BW#CZSM)T zTN<6oW1YF&m`jsjGadA>^Z~jb62O#|0p5|4YE&15JxzMWmFs)JL_)>sY*UZpKo@8J zxMSJAu2(##{2TP}_Ep+ncFB@yBLC}?-9fabJL?oSsO{?ZWS{kG5XaI4pT=!2*r)mA zz^gwz=op#nl;sW2q^yZrxApb`pVyo6QZzF4VVanl+)-9vQN}mB?4`f${`FzcwXrBN zJ!=hTZP#z#b8b{kKSdBRJ!0j`Vr-}O3s>_YJEy!eyl9uMoc%YebL864gPP9r-QwHd ztW*y=Nq-!B=6*V5e0H_x-AXsFaD6-S#e=!j^n!P-Tl4L_WP*!#7wzkxO>~O$nlV?< z>6YK094qe=-<5P@mWl&3S)^sx{5|4tZMPnV$@flwtX928ip>1yS$~v&e(SsXndn_N z;bebHW=3~F>$L$78^cq}4b12CgUVOR?WdpS804kC6K7}p`g=;ON_+--60ZKZ*XG}` z{V%u~2iX7oWA5t+UGU8@?845Y^4qH{7vAVb{~@^3zpnDj9L0sZ8C&)~d()j$wYAsx zThVRwO%y->+OgfJ=-jjEd#!PWFD2=px<=*D4}M?N<#)RX)l!BBKhLhzxL$wOfBQi9 zgVXj#dfJ;8N!o_-il1V1Pz{%Ua5Owyt4JS}end-&d>nsuFFUz)Q2IJ*o4AYhvMiQ0 ze!_tk{_vC-jDG1)b&oFhIOtJTox=s`ln+o_h5mswX^vNoqH0eQ zuBg|m)YS&%2ck%@$>r!dbL5K1-+VYJ#O#=n10lJ)5l0JjvX2;w$&Pi)nYleh+_I zMa#9cO?3LU1{CDJO3U4D3KbEItJ?$KjXt{eNyUpH)?xG$&2L(V>Jy#&HYi5gs8b+g z<|W$Bc0uZ|n(pO_A8N8U9JkN->7jrI#~ zow#{Ze{we2e%}ANOeua>)=TrVOz)yy9JqAnNovD|)rE_ng>9O@lc4XtV|;fv{jK)< zV?7f6Pun^?jp(-`P4MPTIrJRk)t0+|`!>Cx^Xt6XkN4_Nh;N?#Pb>jSjl*+YmL~t?kEebTvD<~}&RR%WQyUyY?L_ei0+1Hon4hgEbXo}Bv7$0N%l-!my!ihT~j zC@0;_PwQkzo!oT_X?>jB)=r_%G@%?Sqg<^}5((j2OkM0Mj|U6wk~=-204tKzb*K z!#43N^i#}r%S@}ac1>u;yf3^N&c?r5Xlr1@Md?L352!$UbmEpJJzPr#%PGv$l9S-F z{<@;0y%aEiiQEyRT8NN=tRt_;zJ5>MMxRP9dEyd~hulijLM_B^s?zc{(+Wf9x_ztn zEqmu_x!iH}&t${jEUA~d0|EGpcOx6fS5R#G$Xb_>0x%A_L}psS&2g=H;*v@4Jo4o~ zJ4{ne{XI7&e~y;b5Y->ySX!9s`fvw!HmQp=-~9`UEidvAeRwmUzjL@~fB$S+MjrGZ z!shYTBfp5-AAUKt$vm6Z_Lc1hV8*e|I=1J^JD z$-cj5Dqcqvq7{Lq=i&jGH^nL$1J$^oUBFVeh3VvbT-G<}f0Ir$lAp+{c=vy4FXf5$ zVAAzAac%vE|q7m+?(q!56|hkH|iwIjuPRXVBC(pWKvOe#(=- z-Q}lFwpQ8ZTDp>#aayzg>~#Eg?J!i*i=!(yBA(7C^28xk^K0XxWS`Z^b=|hCuear_ zpxy8PdT8^l&YZQm<}$-1n?4e{ zVpcGCyU0Uz&$FNtPybn%r;lvNK0im}%K3OEC#dfad)0J}?7naF8c!sqfdL1n6 zRp9|o*!rXGuDYEr(v?G2Q+z;FO?I2?QBX*zyhWJS=&cJ@7GmlW`GO`RcfOb{Mg$!p%-8@9`NmaM0uLIZQ_c7>t@N4-iC6 ze~5JG#XMk~Q?IhqCJHT=S!deu_Nw0G=#J!b>_)bmD=%p#l%x7+@k8yB@w!+(H9ET^ zRIc)!Lc04RG@;8b$@bKJBO9pGhTa0Ku=e9@eS>5FEI;J^ zXiU%(zO-KjImGTRI@&&zC>@-3YpEv7Bpr_oZ&%Hyd|V3D@{_c*Nn>(#qj3H8@Oiz* zr$wjn_ZI$IT45YddjCxJCc~ArJzKJMa8sv{Ch!lMLy)kP zx!SFV6t+-H>YMR;P*JE%Ya(D&tsWs}CW+?bH59Z<;iHDt~SJ2&%8jC64NR&X2U$BhTtCaK5rFa*|3YKj|qf>YuyXkCng3F5b zAWj{fqr*hy`#Ydl$Y^C#$OPtT@xkm=w^_=(ajjD=pe0I?Y*U!5CUkI{lYGyvJof2p zDeDivZSR`sTKpn@i7^QAvTZ^R~7@Ypj2&(m{&Gp%&T9e1tJ3)h6@Fd>6dy%2K(d-u@Zp_dG@hKwT zsw8%(nLD8wW1nx83w80{a-IKWdaN&n^QUJRjICEyeS= zY~GGJOkIO+hm^WzhCKk7CFPL7i2xBJOgPBrP!?X-A^=Simr;%ZBnlKq&SoMZW6q%T zSVRF97nkb+J?X$j6M7a2tV`pHIHE<6P@awuv@;-K6JBsxB#1CbmXh;XTjV0iJ7k14 zZR)N{<{249B3yI95SwCrT!jNcOJH+Kow>Cy|(# z^36cI-ZZ*i5nY<#vUH>Qk~S-}gVau*)GU_oiJD%YZ3;p6=K@)!#JFy$X*(?}%q5*p z{;_i(wRh8W;p2y@Mds3c=FxNhP6Y%fL>AuUVdb|a zm{zo^^o+`5KbAq3ecazohMOi7^4k09uh{I^JjTVgr~vvda$5h4u)d3BC`NZ^*Ftzs zOtjR1nWe>FfXhK}@|Q}Zezi*I3GJcy*#JW&k`-yeE@gpeC6WLl?t`3_;N<>NBw{5? ziRq?T2>~XC0Spxgh(I@RsZ=|Ti%7ut1~J8wmVz%8NC^gT8rCCZD{vx!y_hH=k{Q?( z$JX0nh~gj2Wz{~=>L!(6V62x0RUUteLru&S0csRN{En}H)s0S(@X@ScZ~%wuYn_(d zfmCKH$BDbOU$LuQtT6`e*NU07t9|jw#wCCfx!&vlk{pwCU`|C_q^Qu)RviQbSk1(% z-<=_cS`Kz-J6<2%s_fg)w_$L@Akj~MoPuUW#eRy4r%hWQFb)(gB&-f>6cZhJZf5Hy zt1Qo@J0AQn#YT5YsmVKDSC7R?4NbbkJ05`666K+GH7_1uMn8Y){Vr#tKe!jn!#mg- zxJi}hb=dh}lAPR0exT3vIBY2#PMG=MnB_BqBPGsW$NLR8%6&*_gZcX485y*UhDi<#% zk|hv9Bag%d600I1LtWwkMi^ETMLa~#pjZPmvH|GH8o52_!y9~R4z;qGi;i(y5X5q% zAn@k=)p}79U(KCPI&rq>6-?+%YA{nU# zD?3F4bl*j$3mT)laoyr|zGEy=)A^c{MTO47_x*pxG)0b8s5kZnqsa*5rv&W9V!`44Z`u*EK){>8mn=rUfDhG!fVY@txu9h%Wyf(A zkv1Z7i59dB`An#cq}7?hJe@S3uZ50!F;{D580beRl@D-6%tz;y9>?JUl|2610x8(W0v@qLi^Q1?cZ501%INBi*k<;IT(y=6PH;)nDLl@^rGN29)? zs&Wb&jaQ00ubdgcFcq40W+yDJ16kTS<{TR|#=I^&iP6s;Jk>r3=T12GolBVrPSLK+^}DctAI`0(@#AUW1tnK4X7H?gBR?;sL+9bm2)E=>?;F zhUsSqAXN+i?UZH^K$4C2e}D*cjF5B4PSzmW3snNb0nNio6KV%30a?Lj z+7&xn780?z8koK<$S`gYf^rBX?Hr~88QTs#Q6RfE^mt80q5$pym)^GA?I8$nPMXQ~V&pfV$cOo;E{^tp>MlP`}h5t{bFy z+d{-h^HPHfCr09SU>*QOZt9k2Fhb0*bT53+keWGJ2MZagJbXFRFVbbZ(SLf0OghED ztwESkLc0AWK+wBLjg`PfBi0D+tfUbFC7724FfK}g^gaOxaDdQaGOnKCL<+1pWO7O| zMBERV?w)*zEds|O9n@Rp1K9;~v