diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml index 433d7e8208..67ff74d4f0 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/pom.xml @@ -189,6 +189,10 @@ language governing permissions and limitations under the License. --> jBcrypt 0.4.1 + + com.google.guava + guava + org.apache.nifi nifi-mock diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TransformXml.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TransformXml.java index 5c3c23ca16..e90a3a1c69 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TransformXml.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/TransformXml.java @@ -28,7 +28,10 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Templates; import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; @@ -41,6 +44,7 @@ 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.PropertyDescriptor; import org.apache.nifi.components.ValidationContext; import org.apache.nifi.components.ValidationResult; @@ -60,6 +64,10 @@ import org.apache.nifi.stream.io.BufferedInputStream; import org.apache.nifi.util.StopWatch; import org.apache.nifi.util.Tuple; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + @EventDriven @SideEffectFree @SupportsBatching @@ -76,13 +84,43 @@ public class TransformXml extends AbstractProcessor { .name("XSLT file name") .description("Provides the name (including full path) of the XSLT file to apply to the flowfile XML content.") .required(true) + .expressionLanguageSupported(true) .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR) .build(); + public static final PropertyDescriptor INDENT_OUTPUT = new PropertyDescriptor.Builder() + .name("indent-output") + .displayName("Indent") + .description("Whether or not to indent the output.") + .required(true) + .defaultValue("true") + .allowableValues("true", "false") + .addValidator(StandardValidators.BOOLEAN_VALIDATOR) + .build(); + + public static final PropertyDescriptor CACHE_SIZE = new PropertyDescriptor.Builder() + .name("cache-size") + .displayName("Cache size") + .description("Maximum number of stylesheets to cache. Zero disables the cache.") + .required(true) + .defaultValue("10") + .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR) + .build(); + + public static final PropertyDescriptor CACHE_TTL_AFTER_LAST_ACCESS = new PropertyDescriptor.Builder() + .name("cache-ttl-after-last-access") + .displayName("Cache TTL after last access") + .description("The cache TTL (time-to-live) or how long to keep stylesheets in the cache after last access.") + .required(true) + .defaultValue("60 secs") + .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR) + .build(); + public static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("The FlowFile with transformed content will be routed to this relationship") .build(); + public static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("If a FlowFile fails processing for any reason (for example, the FlowFile is not valid XML), it will be routed to this relationship") @@ -90,11 +128,15 @@ public class TransformXml extends AbstractProcessor { private List properties; private Set relationships; + private LoadingCache cache; @Override protected void init(final ProcessorInitializationContext context) { final List properties = new ArrayList<>(); properties.add(XSLT_FILE_NAME); + properties.add(INDENT_OUTPUT); + properties.add(CACHE_SIZE); + properties.add(CACHE_TTL_AFTER_LAST_ACCESS); this.properties = Collections.unmodifiableList(properties); final Set relationships = new HashSet<>(); @@ -124,6 +166,35 @@ public class TransformXml extends AbstractProcessor { .build(); } + private Templates newTemplates(String path) throws TransformerConfigurationException { + TransformerFactory factory = TransformerFactory.newInstance(); + return factory.newTemplates(new StreamSource(path)); + } + + @OnScheduled + public void onScheduled(final ProcessContext context) { + final ComponentLog logger = getLogger(); + final Integer cacheSize = context.getProperty(CACHE_SIZE).asInteger(); + final Long cacheTTL = context.getProperty(CACHE_TTL_AFTER_LAST_ACCESS).asTimePeriod(TimeUnit.SECONDS); + + if (cacheSize > 0) { + CacheBuilder cacheBuilder = CacheBuilder.newBuilder().maximumSize(cacheSize); + if (cacheTTL > 0) { + cacheBuilder = cacheBuilder.expireAfterAccess(cacheTTL, TimeUnit.SECONDS); + } + + cache = cacheBuilder.build( + new CacheLoader() { + public Templates load(String path) throws TransformerConfigurationException { + return newTemplates(path); + } + }); + } else { + cache = null; + logger.warn("Stylesheet cache disabled because cache size is set to 0"); + } + } + @Override public void onTrigger(final ProcessContext context, final ProcessSession session) { final FlowFile original = session.get(); @@ -133,17 +204,25 @@ public class TransformXml extends AbstractProcessor { final ComponentLog logger = getLogger(); final StopWatch stopWatch = new StopWatch(true); + final String xsltFileName = context.getProperty(XSLT_FILE_NAME) + .evaluateAttributeExpressions(original) + .getValue(); + final Boolean indentOutput = context.getProperty(INDENT_OUTPUT).asBoolean(); try { FlowFile transformed = session.write(original, new StreamCallback() { @Override public void process(final InputStream rawIn, final OutputStream out) throws IOException { try (final InputStream in = new BufferedInputStream(rawIn)) { + final Templates templates; + if (cache != null) { + templates = cache.get(xsltFileName); + } else { + templates = newTemplates(xsltFileName); + } - File stylesheet = new File(context.getProperty(XSLT_FILE_NAME).getValue()); - StreamSource styleSource = new StreamSource(stylesheet); - TransformerFactory tfactory = new net.sf.saxon.TransformerFactoryImpl(); - Transformer transformer = tfactory.newTransformer(styleSource); + final Transformer transformer = templates.newTransformer(); + transformer.setOutputProperty(OutputKeys.INDENT, (indentOutput ? "yes" : "no")); // pass all dynamic properties to the transformer for (final Map.Entry entry : context.getProperties().entrySet()) { diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTransformXml.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTransformXml.java index 2dbf09f8fc..5ced26657d 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTransformXml.java +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestTransformXml.java @@ -16,12 +16,15 @@ */ package org.apache.nifi.processors.standard; +import java.io.IOException; + import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -29,7 +32,6 @@ import java.util.Map; import org.apache.nifi.util.MockFlowFile; import org.apache.nifi.util.TestRunner; import org.apache.nifi.util.TestRunners; -import org.junit.Ignore; import org.junit.Test; @@ -59,7 +61,6 @@ public class TestTransformXml { original.assertContentEquals("not xml"); } - @Ignore("this test fails") @Test public void testTransformMath() throws IOException { final TestRunner runner = TestRunners.newTestRunner(new TransformXml()); @@ -72,12 +73,11 @@ public class TestTransformXml { runner.assertAllFlowFilesTransferred(TransformXml.REL_SUCCESS); final MockFlowFile transformed = runner.getFlowFilesForRelationship(TransformXml.REL_SUCCESS).get(0); - final String transformedContent = new String(transformed.toByteArray(), StandardCharsets.UTF_8); + final String expectedContent = new String(Files.readAllBytes(Paths.get("src/test/resources/TestTransformXml/math.html"))).trim(); - transformed.assertContentEquals(Paths.get("src/test/resources/TestTransformXml/math.html")); + transformed.assertContentEquals(expectedContent); } - @Ignore("this test fails") @Test public void testTransformCsv() throws IOException { final TestRunner runner = TestRunners.newTestRunner(new TransformXml()); @@ -108,9 +108,44 @@ public class TestTransformXml { runner.assertAllFlowFilesTransferred(TransformXml.REL_SUCCESS); final MockFlowFile transformed = runner.getFlowFilesForRelationship(TransformXml.REL_SUCCESS).get(0); final String transformedContent = new String(transformed.toByteArray(), StandardCharsets.ISO_8859_1); + final String expectedContent = new String(Files.readAllBytes(Paths.get("src/test/resources/TestTransformXml/tokens.xml"))); - transformed.assertContentEquals(Paths.get("src/test/resources/TestTransformXml/tokens.xml")); + transformed.assertContentEquals(expectedContent); } } + @Test + public void testTransformExpressionLanguage() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new TransformXml()); + runner.setProperty("header", "Test for mod"); + runner.setProperty(TransformXml.XSLT_FILE_NAME, "${xslt.path}"); + + final Map attributes = new HashMap<>(); + attributes.put("xslt.path", "src/test/resources/TestTransformXml/math.xsl"); + runner.enqueue(Paths.get("src/test/resources/TestTransformXml/math.xml"), attributes); + runner.run(); + + runner.assertAllFlowFilesTransferred(TransformXml.REL_SUCCESS); + final MockFlowFile transformed = runner.getFlowFilesForRelationship(TransformXml.REL_SUCCESS).get(0); + final String expectedContent = new String(Files.readAllBytes(Paths.get("src/test/resources/TestTransformXml/math.html"))).trim(); + + transformed.assertContentEquals(expectedContent); + } + + @Test + public void testTransformNoCache() throws IOException { + final TestRunner runner = TestRunners.newTestRunner(new TransformXml()); + runner.setProperty("header", "Test for mod"); + runner.setProperty(TransformXml.CACHE_SIZE, "0"); + runner.setProperty(TransformXml.XSLT_FILE_NAME, "src/test/resources/TestTransformXml/math.xsl"); + runner.enqueue(Paths.get("src/test/resources/TestTransformXml/math.xml")); + runner.run(); + + runner.assertAllFlowFilesTransferred(TransformXml.REL_SUCCESS); + final MockFlowFile transformed = runner.getFlowFilesForRelationship(TransformXml.REL_SUCCESS).get(0); + final String expectedContent = new String(Files.readAllBytes(Paths.get("src/test/resources/TestTransformXml/math.html"))).trim(); + + transformed.assertContentEquals(expectedContent); + } + } diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.html b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.html index 8f58b6ea52..254d6a381a 100755 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.html +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.html @@ -1,8 +1,8 @@ -

Test for mod

-
-

Should say "1": 1

-

Should say "1": 1

-

Should say "-1": -1

-

true

- \ No newline at end of file +

Test for mod

+
+

Should say "1": 1

+

Should say "1": 1

+

Should say "-1": -1

+

true

+ diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.xsl b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.xsl index b17f91bfe5..5c5a0b6c06 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.xsl +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/math.xsl @@ -13,10 +13,10 @@ See the License for the specific language governing permissions and limitations under the License. --> - - + @@ -33,4 +33,4 @@

-
\ No newline at end of file + diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/tokens.xml b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/tokens.xml index 8dfe80b701..b7d8efc69d 100644 --- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/tokens.xml +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/resources/TestTransformXml/tokens.xml @@ -1,17 +1,17 @@ - - 1 - 2 - 3 - 4 - C:\dir$abc - 6 - 7 - A,B - "don't" - 2014-05-01T30:23:00Z - 11 - 12 - + + 1 + 2 + 3 + 4 + C:\dir$abc + 6 + 7 + A,B + "don't" + 2014-05-01T30:23:00Z + 11 + 12 +