Repository: nifi Updated Branches: refs/heads/master 903f4981a -> ee14d8f9d
http://git-wip-us.apache.org/repos/asf/nifi/blob/f378ee90/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ReplaceText.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ReplaceText.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ReplaceText.java index fb51d45..3cb7eda 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ReplaceText.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ReplaceText.java @@ -25,17 +25,19 @@ import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; 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.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.commons.io.IOUtils; import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; -import org.apache.commons.io.IOUtils; import org.apache.nifi.annotation.behavior.SideEffectFree; import org.apache.nifi.annotation.behavior.SupportsBatching; import org.apache.nifi.annotation.documentation.CapabilityDescription; @@ -53,6 +55,7 @@ import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.ProcessorInitializationContext; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.InputStreamCallback; import org.apache.nifi.processor.io.OutputStreamCallback; import org.apache.nifi.processor.io.StreamCallback; import org.apache.nifi.processor.util.FlowFileFilters; @@ -77,7 +80,8 @@ public class ReplaceText extends AbstractProcessor { public static final String appendValue = "Append"; public static final String regexReplaceValue = "Regex Replace"; public static final String literalReplaceValue = "Literal Replace"; - private final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)"); + public static final String alwaysReplace = "Always Replace"; + private static final Pattern backReferencePattern = Pattern.compile("\\$(\\d+)"); private static final String DEFAULT_REGEX = "(?s:^.*$)"; private static final String DEFAULT_REPLACEMENT_VALUE = "$1"; @@ -95,6 +99,9 @@ public class ReplaceText extends AbstractProcessor { "Interpret the Search Value as a Regular Expression and replace all matches with the Replacement Value. The Replacement Value may reference Capturing Groups used " + "in the Search Value by using a dollar-sign followed by the Capturing Group number, such as $1 or $2. If the Search Value is set to .* then everything is replaced without " + "even evaluating the Regular Expression."); + static final AllowableValue ALWAYS_REPLACE = new AllowableValue(alwaysReplace, alwaysReplace, + "Always replaces the entire line or the entire contents of the FlowFile (depending on the value of the <Evaluation Mode> property) and does not bother searching " + + "for any value. When this strategy is chosen, the <Search Value> property is ignored."); public static final PropertyDescriptor SEARCH_VALUE = new PropertyDescriptor.Builder() .name("Regular Expression") @@ -108,7 +115,9 @@ public class ReplaceText extends AbstractProcessor { public static final PropertyDescriptor REPLACEMENT_VALUE = new PropertyDescriptor.Builder() .name("Replacement Value") .description("The value to insert using the 'Replacement Strategy'. Using \"Regex Replace\" back-references to Regular Expression capturing groups " - + "are supported, but back-references that reference capturing groups that do not exist in the regular expression will be treated as literal value.") + + "are supported, but back-references that reference capturing groups that do not exist in the regular expression will be treated as literal value. " + + "Back References may also be referenced using the Expression Language, as '$1', '$2', etc. The single-tick marks MUST be included, as these variables are " + + "not \"Standard\" attribute names (attribute names must be quoted unless they contain only numbers, letters, and _).") .required(true) .defaultValue(DEFAULT_REPLACEMENT_VALUE) .addValidator(Validator.VALID) @@ -124,11 +133,11 @@ public class ReplaceText extends AbstractProcessor { public static final PropertyDescriptor MAX_BUFFER_SIZE = new PropertyDescriptor.Builder() .name("Maximum Buffer Size") .description("Specifies the maximum amount of data to buffer (per file or per line, depending on the Evaluation Mode) in order to " - + "apply the regular expressions. If 'Entire Text' (in Evaluation Mode) is selected and the FlowFile is larger than this value, " + + "apply the replacement. If 'Entire Text' (in Evaluation Mode) is selected and the FlowFile is larger than this value, " + "the FlowFile will be routed to 'failure'. " + "In 'Line-by-Line' Mode, if a single line is larger than this value, the FlowFile will be routed to 'failure'. A default value " + "of 1 MB is provided, primarily for 'Entire Text' mode. In 'Line-by-Line' Mode, a value such as 8 KB or 16 KB is suggested. " - + "This value is ignored and the buffer is not used if 'Regular Expression' is set to '.*'") + + "This value is ignored if the <Replacement Strategy> property is set to one of: Append, Prepend, Always Replace") .required(true) .addValidator(StandardValidators.DATA_SIZE_VALIDATOR) .defaultValue("1 MB") @@ -136,7 +145,7 @@ public class ReplaceText extends AbstractProcessor { public static final PropertyDescriptor REPLACEMENT_STRATEGY = new PropertyDescriptor.Builder() .name("Replacement Strategy") .description("The strategy for how and what to replace within the FlowFile's text content.") - .allowableValues(PREPEND, APPEND, REGEX_REPLACE, LITERAL_REPLACE) + .allowableValues(PREPEND, APPEND, REGEX_REPLACE, LITERAL_REPLACE, ALWAYS_REPLACE) .defaultValue(REGEX_REPLACE.getValue()) .required(true) .build(); @@ -210,18 +219,6 @@ public class ReplaceText extends AbstractProcessor { return; } - final AttributeValueDecorator escapeBackRefDecorator = new AttributeValueDecorator() { - @Override - public String decorate(final String attributeValue) { - return attributeValue.replace("$", "\\$"); - } - }; - - final String regexValue = context.getProperty(SEARCH_VALUE).evaluateAttributeExpressions().getValue(); - final int numCapturingGroups = Pattern.compile(regexValue).matcher("").groupCount(); - - final boolean skipBuffer = ".*".equals(unsubstitutedRegex); - final Charset charset = Charset.forName(context.getProperty(CHARACTER_SET).getValue()); final int maxBufferSize = context.getProperty(MAX_BUFFER_SIZE).asDataSize(DataUnit.B).intValue(); @@ -233,82 +230,151 @@ public class ReplaceText extends AbstractProcessor { buffer = null; } + ReplacementStrategyExecutor replacementStrategyExecutor; + switch (replacementStrategy) { + case prependValue: + replacementStrategyExecutor = new PrependReplace(); + break; + case appendValue: + replacementStrategyExecutor = new AppendReplace(); + break; + case regexReplaceValue: + // for backward compatibility - if replacement regex is ".*" then we will simply always replace the content. + if (context.getProperty(SEARCH_VALUE).getValue().equals(".*")) { + replacementStrategyExecutor = new AlwaysReplace(); + } else { + replacementStrategyExecutor = new RegexReplace(buffer, context); + } + + break; + case literalReplaceValue: + replacementStrategyExecutor = new LiteralReplace(buffer); + break; + case alwaysReplace: + replacementStrategyExecutor = new AlwaysReplace(); + break; + default: + throw new AssertionError(); + } + for (FlowFile flowFile : flowFiles) { if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { - if (flowFile.getSize() > maxBufferSize && !skipBuffer) { + if (flowFile.getSize() > maxBufferSize && replacementStrategyExecutor.isAllDataBufferedForEntireText()) { session.transfer(flowFile, REL_FAILURE); continue; } } - String replacement; - if (!replacementStrategy.equals(regexReplaceValue)) { - replacement = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile).getValue(); - } else { - replacement = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile, escapeBackRefDecorator).getValue(); - final Matcher backRefMatcher = backReferencePattern.matcher(replacement); - while (backRefMatcher.find()) { - final String backRefNum = backRefMatcher.group(1); - if (backRefNum.startsWith("0")) { - continue; - } - final int originalBackRefIndex = Integer.parseInt(backRefNum); - int backRefIndex = originalBackRefIndex; - - // if we have a replacement value like $123, and we have less than 123 capturing groups, then - // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups, - // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then - // we want to truncate the 1 and get 0. - while (backRefIndex > numCapturingGroups && backRefIndex >= 10) { - backRefIndex /= 10; - } + final StopWatch stopWatch = new StopWatch(true); - if (backRefIndex > numCapturingGroups) { - final StringBuilder sb = new StringBuilder(replacement.length() + 1); - final int groupStart = backRefMatcher.start(1); + flowFile = replacementStrategyExecutor.replace(flowFile, session, context, evaluateMode, charset, maxBufferSize); - sb.append(replacement.substring(0, groupStart - 1)); - sb.append("\\"); - sb.append(replacement.substring(groupStart - 1)); - replacement = sb.toString(); - } - } + logger.info("Transferred {} to 'success'", new Object[] {flowFile}); + session.getProvenanceReporter().modifyContent(flowFile, stopWatch.getElapsed(TimeUnit.MILLISECONDS)); + session.transfer(flowFile, REL_SUCCESS); + } + } + + + // If we find a back reference that is not valid, then we will treat it as a literal string. For example, if we have 3 capturing + // groups and the Replacement Value has the value is "I owe $8 to him", then we want to treat the $8 as a literal "$8", rather + // than attempting to use it as a back reference. + private static String escapeLiteralBackReferences(final String unescaped, final int numCapturingGroups) { + if (numCapturingGroups == 0) { + return unescaped; + } + + String value = unescaped; + final Matcher backRefMatcher = backReferencePattern.matcher(value); + while (backRefMatcher.find()) { + final String backRefNum = backRefMatcher.group(1); + if (backRefNum.startsWith("0")) { + continue; + } + final int originalBackRefIndex = Integer.parseInt(backRefNum); + int backRefIndex = originalBackRefIndex; + + // if we have a replacement value like $123, and we have less than 123 capturing groups, then + // we want to truncate the 3 and use capturing group 12; if we have less than 12 capturing groups, + // then we want to truncate the 2 and use capturing group 1; if we don't have a capturing group then + // we want to truncate the 1 and get 0. + while (backRefIndex > numCapturingGroups && backRefIndex >= 10) { + backRefIndex /= 10; } - ReplacementStrategyExecutor replacementStrategyExecutor; - switch (replacementStrategy) { - case prependValue: - replacementStrategyExecutor = new PrependReplace(); - break; - case appendValue: - replacementStrategyExecutor = new AppendReplace(); - break; - case regexReplaceValue: - replacementStrategyExecutor = new RegexReplace(buffer); - break; - case literalReplaceValue: - replacementStrategyExecutor = new LiteralReplace(buffer); - break; - default: - throw new AssertionError(); + if (backRefIndex > numCapturingGroups) { + final StringBuilder sb = new StringBuilder(value.length() + 1); + final int groupStart = backRefMatcher.start(1); + + sb.append(value.substring(0, groupStart - 1)); + sb.append("\\"); + sb.append(value.substring(groupStart - 1)); + value = sb.toString(); } + } - final StopWatch stopWatch = new StopWatch(true); + return value; + } - flowFile = replacementStrategyExecutor.replace(flowFile, session, context, replacement, evaluateMode, - charset, maxBufferSize, skipBuffer); + private static class AlwaysReplace implements ReplacementStrategyExecutor { + @Override + public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String evaluateMode, final Charset charset, final int maxBufferSize) { - logger.info("Transferred {} to 'success'", new Object[] {flowFile}); - session.getProvenanceReporter().modifyContent(flowFile, stopWatch.getElapsed(TimeUnit.MILLISECONDS)); - session.transfer(flowFile, REL_SUCCESS); + final String replacementValue = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile).getValue(); + final StringBuilder lineEndingBuilder = new StringBuilder(2); + + if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { + flowFile = session.write(flowFile, new StreamCallback() { + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + out.write(replacementValue.getBytes(charset)); + } + }); + } else { + flowFile = session.write(flowFile, new StreamCallback() { + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + try (NLKBufferedReader br = new NLKBufferedReader(new InputStreamReader(in, charset), maxBufferSize); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, charset));) { + + String line; + while ((line = br.readLine()) != null) { + // We need to determine what line ending was used and use that after our replacement value. + lineEndingBuilder.setLength(0); + for (int i = line.length() - 1; i >= 0; i--) { + final char c = line.charAt(i); + if (c == '\r' || c == '\n') { + lineEndingBuilder.append(c); + } else { + break; + } + } + + bw.write(replacementValue); + + // Preserve original line endings. Reverse string because we iterated over original line ending in reverse order, appending to builder. + // So if builder has multiple characters, they are now reversed from the original string's ordering. + bw.write(lineEndingBuilder.reverse().toString()); + } + } + } + }); + } + + return flowFile; + } + + @Override + public boolean isAllDataBufferedForEntireText() { + return false; } } private static class PrependReplace implements ReplacementStrategyExecutor { - @Override - public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String replacementValue, final String evaluateMode, - final Charset charset, final int maxBufferSize, final boolean skipBuffer) { + public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String evaluateMode, final Charset charset, final int maxBufferSize) { + final String replacementValue = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile).getValue(); + if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { flowFile = session.write(flowFile, new StreamCallback() { @Override @@ -334,13 +400,19 @@ public class ReplaceText extends AbstractProcessor { } return flowFile; } + + @Override + public boolean isAllDataBufferedForEntireText() { + return false; + } } private static class AppendReplace implements ReplacementStrategyExecutor { @Override - public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String replacementValue, final String evaluateMode, - final Charset charset, final int maxBufferSize, final boolean skipBuffer) { + public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String evaluateMode, final Charset charset, final int maxBufferSize) { + final String replacementValue = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile).getValue(); + if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { flowFile = session.write(flowFile, new StreamCallback() { @Override @@ -387,81 +459,121 @@ public class ReplaceText extends AbstractProcessor { } return flowFile; } + + @Override + public boolean isAllDataBufferedForEntireText() { + return false; + } } private static class RegexReplace implements ReplacementStrategyExecutor { private final byte[] buffer; + private final int numCapturingGroups; + private final Map<String, String> additionalAttrs; + + private static final AttributeValueDecorator escapeBackRefDecorator = new AttributeValueDecorator() { + @Override + public String decorate(final String attributeValue) { + return attributeValue.replace("$", "\\$"); + } + }; - public RegexReplace(final byte[] buffer) { + public RegexReplace(final byte[] buffer, final ProcessContext context) { this.buffer = buffer; + + final String regexValue = context.getProperty(SEARCH_VALUE).evaluateAttributeExpressions().getValue(); + numCapturingGroups = Pattern.compile(regexValue).matcher("").groupCount(); + additionalAttrs = new HashMap<>(numCapturingGroups); } @Override - public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String replacementValue, final String evaluateMode, - final Charset charset, final int maxBufferSize, final boolean skipBuffer) { - final String replacementFinal = replacementValue.replaceAll("(\\$\\D)", "\\\\$1"); - - // always match; just overwrite value with the replacement value; this optimization prevents us - // from reading the file at all. - if (skipBuffer) { - if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { - flowFile = session.write(flowFile, new OutputStreamCallback() { - @Override - public void process(final OutputStream out) throws IOException { - out.write(replacementFinal.getBytes(charset)); - } - }); - } else { - flowFile = session.write(flowFile, new StreamCallback() { - @Override - public void process(final InputStream in, final OutputStream out) throws IOException { - try (NLKBufferedReader br = new NLKBufferedReader(new InputStreamReader(in, charset), maxBufferSize); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, charset));) { - while (null != br.readLine()) { - bw.write(replacementFinal); - } - } - } - }); + public FlowFile replace(final FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String evaluateMode, final Charset charset, final int maxBufferSize) { + final AttributeValueDecorator quotedAttributeDecorator = new AttributeValueDecorator() { + @Override + public String decorate(final String attributeValue) { + return Pattern.quote(attributeValue); } - } else { - final AttributeValueDecorator quotedAttributeDecorator = new AttributeValueDecorator() { + }; + final String searchRegex = context.getProperty(SEARCH_VALUE).evaluateAttributeExpressions(flowFile, quotedAttributeDecorator).getValue(); + final Pattern searchPattern = Pattern.compile(searchRegex); + + final int flowFileSize = (int) flowFile.getSize(); + FlowFile updatedFlowFile; + if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { + session.read(flowFile, new InputStreamCallback() { @Override - public String decorate(final String attributeValue) { - return Pattern.quote(attributeValue); + public void process(final InputStream in) throws IOException { + StreamUtils.fillBuffer(in, buffer, false); } - }; - final String searchRegex = context.getProperty(SEARCH_VALUE).evaluateAttributeExpressions(flowFile, quotedAttributeDecorator).getValue(); + }); + + final String contentString = new String(buffer, 0, flowFileSize, charset); + additionalAttrs.clear(); + final Matcher matcher = searchPattern.matcher(contentString); + if (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + final String groupValue = matcher.group(i); + additionalAttrs.put("$" + i, groupValue); + } + + String replacement = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile, additionalAttrs, escapeBackRefDecorator).getValue(); + replacement = escapeLiteralBackReferences(replacement, numCapturingGroups); + + // If we have a $ followed by anything other than a number, then escape it. E.g., $d becomes \$d so that it can be used as a literal in a regex. + final String replacementFinal = replacement.replaceAll("(\\$\\D)", "\\\\$1"); - final int flowFileSize = (int) flowFile.getSize(); - if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { - flowFile = session.write(flowFile, new StreamCallback() { + final String updatedValue = contentString.replaceAll(searchRegex, replacementFinal); + updatedFlowFile = session.write(flowFile, new OutputStreamCallback() { @Override - public void process(final InputStream in, final OutputStream out) throws IOException { - StreamUtils.fillBuffer(in, buffer, false); - final String contentString = new String(buffer, 0, flowFileSize, charset); - final String updatedValue = contentString.replaceAll(searchRegex, replacementFinal); + public void process(final OutputStream out) throws IOException { out.write(updatedValue.getBytes(charset)); } }); } else { - flowFile = session.write(flowFile, new StreamCallback() { - @Override - public void process(final InputStream in, final OutputStream out) throws IOException { - try (NLKBufferedReader br = new NLKBufferedReader(new InputStreamReader(in, charset), maxBufferSize); - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, charset))) { - String oneLine; - while (null != (oneLine = br.readLine())) { + // If no match, just return the original. No need to write out any content. + return flowFile; + } + } else { + updatedFlowFile = session.write(flowFile, new StreamCallback() { + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + try (NLKBufferedReader br = new NLKBufferedReader(new InputStreamReader(in, charset), maxBufferSize); + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(out, charset))) { + String oneLine; + while (null != (oneLine = br.readLine())) { + additionalAttrs.clear(); + final Matcher matcher = searchPattern.matcher(oneLine); + if (matcher.find()) { + for (int i = 1; i <= matcher.groupCount(); i++) { + final String groupValue = matcher.group(i); + additionalAttrs.put("$" + i, groupValue); + } + + String replacement = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile, additionalAttrs, escapeBackRefDecorator).getValue(); + replacement = escapeLiteralBackReferences(replacement, numCapturingGroups); + + // If we have a $ followed by anything other than a number, then escape it. E.g., $d becomes \$d so that it can be used as a literal in a regex. + final String replacementFinal = replacement.replaceAll("(\\$\\D)", "\\\\$1"); + final String updatedValue = oneLine.replaceAll(searchRegex, replacementFinal); bw.write(updatedValue); + } else { + // No match. Just write out the line as it was. + bw.write(oneLine); } } } - }); - } + } + }); } - return flowFile; + + return updatedFlowFile; + } + + @Override + public boolean isAllDataBufferedForEntireText() { + return true; } } @@ -473,8 +585,10 @@ public class ReplaceText extends AbstractProcessor { } @Override - public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String replacementValue, final String evaluateMode, - final Charset charset, final int maxBufferSize, final boolean skipBuffer) { + public FlowFile replace(FlowFile flowFile, final ProcessSession session, final ProcessContext context, final String evaluateMode, final Charset charset, final int maxBufferSize) { + + final String replacementValue = context.getProperty(REPLACEMENT_VALUE).evaluateAttributeExpressions(flowFile).getValue(); + final AttributeValueDecorator quotedAttributeDecorator = new AttributeValueDecorator() { @Override public String decorate(final String attributeValue) { @@ -484,7 +598,6 @@ public class ReplaceText extends AbstractProcessor { final String searchValue = context.getProperty(SEARCH_VALUE).evaluateAttributeExpressions(flowFile, quotedAttributeDecorator).getValue(); - final int flowFileSize = (int) flowFile.getSize(); if (evaluateMode.equalsIgnoreCase(ENTIRE_TEXT)) { flowFile = session.write(flowFile, new StreamCallback() { @@ -515,9 +628,16 @@ public class ReplaceText extends AbstractProcessor { } return flowFile; } + + @Override + public boolean isAllDataBufferedForEntireText() { + return true; + } } private interface ReplacementStrategyExecutor { - FlowFile replace(FlowFile flowFile, ProcessSession session, ProcessContext context, String replacement, String evaluateMode, Charset charset, int maxBufferSize, boolean skipBuffer); + FlowFile replace(FlowFile flowFile, ProcessSession session, ProcessContext context, String evaluateMode, Charset charset, int maxBufferSize); + + boolean isAllDataBufferedForEntireText(); } } http://git-wip-us.apache.org/repos/asf/nifi/blob/f378ee90/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestReplaceText.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestReplaceText.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestReplaceText.java index 3a311a3..561a1e0 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestReplaceText.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestReplaceText.java @@ -914,13 +914,13 @@ public class TestReplaceText { final Map<String, String> attributes = new HashMap<>(); attributes.put("abc", "Good"); - runner.enqueue(translateNewLines(Paths.get("src/test/resources/TestReplaceTextLineByLine/testFile.txt")), attributes); + runner.enqueue(Paths.get("src/test/resources/TestReplaceTextLineByLine/testFile.txt"), attributes); runner.run(); runner.assertAllFlowFilesTransferred(ReplaceText.REL_SUCCESS, 1); final MockFlowFile out = runner.getFlowFilesForRelationship(ReplaceText.REL_SUCCESS).get(0); - out.assertContentEquals(translateNewLines(new File("src/test/resources/TestReplaceTextLineByLine/Good.txt"))); + out.assertContentEquals("Good\nGood\nGood\nGood\nGood\nGood\nGood\nGood\nGood\nGood\nGood"); } @Test @@ -939,7 +939,7 @@ public class TestReplaceText { runner.assertAllFlowFilesTransferred(ReplaceText.REL_SUCCESS, 1); final MockFlowFile out = runner.getFlowFilesForRelationship(ReplaceText.REL_SUCCESS).get(0); - out.assertContentEquals("GoodGoodGood"); + out.assertContentEquals("Good\r\nGood\r\nGood\r"); } @Test @@ -984,17 +984,108 @@ public class TestReplaceText { System.out.println(outContent); Assert.assertTrue(outContent.equals("attribute header\n\nabc.txt\n\ndata header\n\nHello\n\n\nfooter\n" + "attribute header\n\nabc.txt\n\ndata header\n\nWorld!\n\nfooter\n")); + } + + @Test + public void testCapturingGroupInExpressionLanguage() { + final TestRunner runner = TestRunners.newTestRunner(new ReplaceText()); + runner.setValidateExpressionUsage(false); + runner.setProperty(ReplaceText.EVALUATION_MODE, ReplaceText.LINE_BY_LINE); + runner.setProperty(ReplaceText.SEARCH_VALUE, "(.*?),(.*?),(\\d+.*)"); + runner.setProperty(ReplaceText.REPLACEMENT_VALUE, "$1,$2,${ '$3':toDate('ddMMMyyyy'):format('yyyy/MM/dd') }"); + + final String csvIn = + "2006,10-01-2004,10may2004\n" + + "2007,15-05-2006,10jun2005\r\n" + + "2009,8-8-2008,10aug2008"; + final String expectedCsvOut = + "2006,10-01-2004,2004/05/10\n" + + "2007,15-05-2006,2005/06/10\r\n" + + "2009,8-8-2008,2008/08/10"; + + runner.enqueue(csvIn.getBytes()); + + runner.run(); + + runner.assertAllFlowFilesTransferred(ReplaceText.REL_SUCCESS, 1); + final MockFlowFile out = runner.getFlowFilesForRelationship(ReplaceText.REL_SUCCESS).get(0); + out.assertContentEquals(expectedCsvOut); + } + + @Test + public void testCapturingGroupInExpressionLanguage2() { + final TestRunner runner = TestRunners.newTestRunner(new ReplaceText()); + runner.setValidateExpressionUsage(false); + runner.setProperty(ReplaceText.EVALUATION_MODE, ReplaceText.LINE_BY_LINE); + runner.setProperty(ReplaceText.SEARCH_VALUE, "(.*)/(.*?).jpg"); + runner.setProperty(ReplaceText.REPLACEMENT_VALUE, "$1/${ '$2':substring(0,1) }.png"); + + final String csvIn = + "1,2,3,https://123.jpg,[email protected]\n" + + "3,2,1,https://321.jpg,[email protected]"; + final String expectedCsvOut = + "1,2,3,https://1.png,[email protected]\n" + + "3,2,1,https://3.png,[email protected]"; + + runner.enqueue(csvIn.getBytes()); + + runner.run(); + + runner.assertAllFlowFilesTransferred(ReplaceText.REL_SUCCESS, 1); + final MockFlowFile out = runner.getFlowFilesForRelationship(ReplaceText.REL_SUCCESS).get(0); + out.assertContentEquals(expectedCsvOut); + } + @Test + public void testAlwaysReplaceEntireText() { + final TestRunner runner = TestRunners.newTestRunner(new ReplaceText()); + runner.setValidateExpressionUsage(false); + runner.setProperty(ReplaceText.EVALUATION_MODE, ReplaceText.ENTIRE_TEXT); + runner.setProperty(ReplaceText.REPLACEMENT_STRATEGY, ReplaceText.ALWAYS_REPLACE); + runner.setProperty(ReplaceText.SEARCH_VALUE, "i do not exist anywhere in the text"); + runner.setProperty(ReplaceText.REPLACEMENT_VALUE, "${filename}"); + + final Map<String, String> attributes = new HashMap<>(); + attributes.put("filename", "abc.txt"); + runner.enqueue("Hello\nWorld!".getBytes(), attributes); + + runner.run(); + + runner.assertAllFlowFilesTransferred(ReplaceText.REL_SUCCESS, 1); + final MockFlowFile out = runner.getFlowFilesForRelationship(ReplaceText.REL_SUCCESS).get(0); + out.assertContentEquals("abc.txt"); + } + + @Test + public void testAlwaysReplaceLineByLine() { + final TestRunner runner = TestRunners.newTestRunner(new ReplaceText()); + runner.setValidateExpressionUsage(false); + runner.setProperty(ReplaceText.EVALUATION_MODE, ReplaceText.LINE_BY_LINE); + runner.setProperty(ReplaceText.REPLACEMENT_STRATEGY, ReplaceText.ALWAYS_REPLACE); + runner.setProperty(ReplaceText.SEARCH_VALUE, "i do not exist anywhere in the text"); + runner.setProperty(ReplaceText.REPLACEMENT_VALUE, "${filename}"); + + final Map<String, String> attributes = new HashMap<>(); + attributes.put("filename", "abc.txt"); + runner.enqueue("Hello\nWorld!\r\ntoday!\n".getBytes(), attributes); + + runner.run(); + + runner.assertAllFlowFilesTransferred(ReplaceText.REL_SUCCESS, 1); + final MockFlowFile out = runner.getFlowFilesForRelationship(ReplaceText.REL_SUCCESS).get(0); + out.assertContentEquals("abc.txt\nabc.txt\r\nabc.txt\n"); } - private byte[] translateNewLines(final File file) throws IOException { + + + private String translateNewLines(final File file) throws IOException { return translateNewLines(file.toPath()); } - private byte[] translateNewLines(final Path path) throws IOException { + private String translateNewLines(final Path path) throws IOException { final byte[] data = Files.readAllBytes(path); final String text = new String(data, StandardCharsets.UTF_8); - return translateNewLines(text).getBytes(StandardCharsets.UTF_8); + return translateNewLines(text); } private String translateNewLines(final String text) { http://git-wip-us.apache.org/repos/asf/nifi/blob/f378ee90/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestReplaceTextLineByLine/Good.txt ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestReplaceTextLineByLine/Good.txt b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestReplaceTextLineByLine/Good.txt deleted file mode 100755 index 6e90a7d..0000000 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestReplaceTextLineByLine/Good.txt +++ /dev/null @@ -1 +0,0 @@ -GoodGoodGoodGoodGoodGoodGoodGoodGoodGoodGood \ No newline at end of file
