You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@nifi.apache.org by al...@apache.org on 2020/09/18 00:56:49 UTC

[nifi] branch support/nifi-1.12.x updated (113050a -> c18a81d)

This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a change to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git.


 discard 113050a  NIFI-7787 fixing version references
 discard 94ad325  NIFI-7799: Relogin with Kerberos on connect exception in DBCPConnectionPool (#4519)
 discard d6d9509  NIFI-7580-Add documentation around autoloading NARs
 discard 9de8759  NIFI-5583: Add cdc processor for MySQL referring to GTID.
 discard 0873ed5  NIFI-7768 Added support for monogdb+srv connection strings.
 discard 57d7ca1  NIFI-7800: Fix line endings for changed files
 discard 5499870  NIFI-7803: Allow ExecuteSQL(Record) properties to evaluate flowfile attributes where possible
 discard 3bb414f  NIFI-7807 Updating in-class documentation to be more clear & adding additionalDetails with examples
 discard 51a3cf4  NIFI-7742: remove defined and null check
 discard 0baf82b  NIFI-7742: update case clause logic
 discard 3a6915d  NIFI-7742: add case for controller service selections
 discard f41adc6  NIFI-7802 Remove commons-configuration2 dependency from nifi-security-utils which ends up nifi-standard-services-api and on the classpath of any standard services
     new c0e7f77  NIFI-7742: add case for controller service selections
     new 57eb740  NIFI-7742: update case clause logic
     new b9b31b2  NIFI-7742: remove defined and null check
     new 33d9b8f  NIFI-7807 Updating in-class documentation to be more clear & adding additionalDetails with examples
     new 6aa58d1  NIFI-7803: Allow ExecuteSQL(Record) properties to evaluate flowfile attributes where possible
     new 05d445f  NIFI-7800: Fix line endings for changed files
     new 2174af7  NIFI-7768 Added support for monogdb+srv connection strings.
     new 1747201  NIFI-5583: Add cdc processor for MySQL referring to GTID.
     new 1adb16e  NIFI-7580-Add documentation around autoloading NARs
     new e952a76  NIFI-7799: Relogin with Kerberos on connect exception in DBCPConnectionPool (#4519)
     new c18a81d  NIFI-7787 fixing version references

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (113050a)
            \
             N -- N -- N   refs/heads/support/nifi-1.12.x (c18a81d)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 nifi-commons/nifi-security-utils/pom.xml           |  5 ++
 .../nifi/security/xml/SafeXMLConfiguration.java    |  0
 nifi-commons/nifi-security-xml-config/pom.xml      | 78 ----------------------
 nifi-commons/pom.xml                               |  1 -
 4 files changed, 5 insertions(+), 79 deletions(-)
 rename nifi-commons/{nifi-security-xml-config => nifi-security-utils}/src/main/java/org/apache/nifi/security/xml/SafeXMLConfiguration.java (100%)
 delete mode 100644 nifi-commons/nifi-security-xml-config/pom.xml


[nifi] 10/11: NIFI-7799: Relogin with Kerberos on connect exception in DBCPConnectionPool (#4519)

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit e952a76e4f9eba5e668ff06af5ff95554b851acf
Author: Matthew Burgess <ma...@apache.org>
AuthorDate: Thu Sep 17 13:33:54 2020 -0400

    NIFI-7799: Relogin with Kerberos on connect exception in DBCPConnectionPool (#4519)
---
 .../{ => nifi-kerberos-test-utils}/pom.xml         | 43 ++++++-----
 .../kerberos/MockKerberosCredentialsService.java   | 85 ++++++++++++++++++++++
 nifi-nar-bundles/nifi-extension-utils/pom.xml      |  1 +
 .../nifi-hive-bundle/nifi-hive3-processors/pom.xml |  6 ++
 .../processors/hive/TestPutHive3Streaming.java     | 67 ++---------------
 .../nifi-dbcp-service/pom.xml                      | 19 +++++
 .../org/apache/nifi/dbcp/DBCPConnectionPool.java   |  9 +++
 .../java/org/apache/nifi/dbcp/DBCPServiceTest.java | 38 ++++++++++
 .../src/test/resources/fake.keytab                 |  0
 9 files changed, 187 insertions(+), 81 deletions(-)

diff --git a/nifi-nar-bundles/nifi-extension-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml
similarity index 57%
copy from nifi-nar-bundles/nifi-extension-utils/pom.xml
copy to nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml
index 2980f23..a223d03 100644
--- a/nifi-nar-bundles/nifi-extension-utils/pom.xml
+++ b/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml
@@ -1,4 +1,4 @@
-<?xml version="1.0"?>
+<?xml version="1.0" encoding="UTF-8"?>
 <!--
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
@@ -17,25 +17,28 @@
     <modelVersion>4.0.0</modelVersion>
     <parent>
         <groupId>org.apache.nifi</groupId>
-        <artifactId>nifi-nar-bundles</artifactId>
-        <version>1.12.1-SNAPSHOT</version>
+        <artifactId>nifi-extension-utils</artifactId>
+        <version>1.13.0-SNAPSHOT</version>
     </parent>
-    <packaging>pom</packaging>
-    <artifactId>nifi-extension-utils</artifactId>
-    <description>
-        This module contains reusable utilities related to extensions that can be shared across NARs.
-    </description>
 
-    <modules>
-        <module>nifi-record-utils</module>
-        <module>nifi-hadoop-utils</module>
-        <module>nifi-processor-utils</module>
-        <module>nifi-reporting-utils</module>
-        <module>nifi-syslog-utils</module>
-        <module>nifi-database-utils</module>
-        <module>nifi-database-test-utils</module>
-        <module>nifi-service-utils</module>
-        <module>nifi-prometheus-utils</module>
-    </modules>
+    <artifactId>nifi-kerberos-test-utils</artifactId>
 
-</project>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-api</artifactId>
+            <version>1.13.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-utils</artifactId>
+            <version>1.13.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-kerberos-credentials-service-api</artifactId>
+            <version>1.13.0-SNAPSHOT</version>
+        </dependency>
+    </dependencies>
+
+</project>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/src/main/java/org/apache/nifi/kerberos/MockKerberosCredentialsService.java b/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/src/main/java/org/apache/nifi/kerberos/MockKerberosCredentialsService.java
new file mode 100644
index 0000000..b7b2a01
--- /dev/null
+++ b/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/src/main/java/org/apache/nifi/kerberos/MockKerberosCredentialsService.java
@@ -0,0 +1,85 @@
+/*
+ * 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.kerberos;
+
+import org.apache.nifi.annotation.lifecycle.OnEnabled;
+import org.apache.nifi.components.PropertyDescriptor;
+import org.apache.nifi.controller.AbstractControllerService;
+import org.apache.nifi.controller.ConfigurationContext;
+import org.apache.nifi.expression.ExpressionLanguageScope;
+import org.apache.nifi.processor.util.StandardValidators;
+import org.apache.nifi.reporting.InitializationException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class MockKerberosCredentialsService extends AbstractControllerService implements KerberosCredentialsService {
+
+    public static String DEFAULT_KEYTAB = "src/test/resources/fake.keytab";
+    public static String DEFAULT_PRINCIPAL = "test@REALM.COM";
+
+    private volatile String keytab = DEFAULT_KEYTAB;
+    private volatile String principal = DEFAULT_PRINCIPAL;
+
+    public static final PropertyDescriptor PRINCIPAL = new PropertyDescriptor.Builder()
+            .name("Kerberos Principal")
+            .description("Kerberos principal to authenticate as. Requires nifi.kerberos.krb5.file to be set in your nifi.properties")
+            .addValidator(StandardValidators.NON_BLANK_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+            .required(true)
+            .build();
+
+    public static final PropertyDescriptor KEYTAB = new PropertyDescriptor.Builder()
+            .name("Kerberos Keytab")
+            .description("Kerberos keytab associated with the principal. Requires nifi.kerberos.krb5.file to be set in your nifi.properties")
+            .addValidator(StandardValidators.FILE_EXISTS_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+            .required(true)
+            .build();
+
+    public MockKerberosCredentialsService() {
+    }
+
+    @OnEnabled
+    public void onConfigured(final ConfigurationContext context) throws InitializationException {
+        keytab = context.getProperty(KEYTAB).getValue();
+        principal = context.getProperty(PRINCIPAL).getValue();
+    }
+
+    @Override
+    public String getKeytab() {
+        return keytab;
+    }
+
+    @Override
+    public String getPrincipal() {
+        return principal;
+    }
+
+    @Override
+    protected List<PropertyDescriptor> getSupportedPropertyDescriptors() {
+        final List<PropertyDescriptor> properties = new ArrayList<>(2);
+        properties.add(KEYTAB);
+        properties.add(PRINCIPAL);
+        return properties;
+    }
+
+    @Override
+    public String getIdentifier() {
+        return "kcs";
+    }
+}
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-extension-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/pom.xml
index 2980f23..17e0572 100644
--- a/nifi-nar-bundles/nifi-extension-utils/pom.xml
+++ b/nifi-nar-bundles/nifi-extension-utils/pom.xml
@@ -36,6 +36,7 @@
         <module>nifi-database-test-utils</module>
         <module>nifi-service-utils</module>
         <module>nifi-prometheus-utils</module>
+        <module>nifi-kerberos-test-utils</module>
     </modules>
 
 </project>
diff --git a/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml b/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml
index edd36f8..38a3234 100644
--- a/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml
@@ -147,6 +147,12 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-kerberos-test-utils</artifactId>
+            <version>1.13.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
diff --git a/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/src/test/java/org/apache/nifi/processors/hive/TestPutHive3Streaming.java b/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/src/test/java/org/apache/nifi/processors/hive/TestPutHive3Streaming.java
index 05e44fb..0db3cad 100644
--- a/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/src/test/java/org/apache/nifi/processors/hive/TestPutHive3Streaming.java
+++ b/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/src/test/java/org/apache/nifi/processors/hive/TestPutHive3Streaming.java
@@ -48,15 +48,11 @@ import org.apache.hive.streaming.StubSerializationError;
 import org.apache.hive.streaming.StubStreamingIOFailure;
 import org.apache.hive.streaming.StubTransactionError;
 import org.apache.nifi.avro.AvroTypeUtil;
-import org.apache.nifi.components.PropertyDescriptor;
-import org.apache.nifi.components.ValidationContext;
-import org.apache.nifi.components.ValidationResult;
-import org.apache.nifi.controller.ControllerService;
-import org.apache.nifi.controller.ControllerServiceInitializationContext;
 import org.apache.nifi.hadoop.SecurityUtil;
 import org.apache.nifi.json.JsonRecordSetWriter;
 import org.apache.nifi.json.JsonTreeReader;
 import org.apache.nifi.kerberos.KerberosCredentialsService;
+import org.apache.nifi.kerberos.MockKerberosCredentialsService;
 import org.apache.nifi.logging.ComponentLog;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.reporting.InitializationException;
@@ -96,7 +92,6 @@ import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.Arrays;
 import java.util.Calendar;
-import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -293,6 +288,8 @@ public class TestPutHive3Streaming {
         KerberosCredentialsService kcs = new MockKerberosCredentialsService();
         runner.addControllerService("kcs", kcs);
         runner.setProperty(KERBEROS_CREDENTIALS_SERVICE, "kcs");
+        runner.setProperty(kcs, MockKerberosCredentialsService.PRINCIPAL, "test");
+        runner.setProperty(kcs, MockKerberosCredentialsService.KEYTAB, "src/test/resources/core-site-security.xml");
         runner.enableControllerService(kcs);
         ugi = mock(UserGroupInformation.class);
         when(hiveConfigurator.authenticate(eq(hiveConf), any(KerberosUser.class))).thenReturn(ugi);
@@ -313,8 +310,10 @@ public class TestPutHive3Streaming {
         runner.setProperty(PutHive3Streaming.HIVE_CONFIGURATION_RESOURCES, "src/test/resources/core-site-security.xml, src/test/resources/hive-site-security.xml");
 
         hiveConf.set(SecurityUtil.HADOOP_SECURITY_AUTHENTICATION, SecurityUtil.KERBEROS);
-        KerberosCredentialsService kcs = new MockKerberosCredentialsService(null, null);
+        KerberosCredentialsService kcs = new MockKerberosCredentialsService();
         runner.addControllerService("kcs", kcs);
+        runner.setProperty(kcs, MockKerberosCredentialsService.PRINCIPAL, "test");
+        runner.setProperty(kcs, MockKerberosCredentialsService.KEYTAB, "src/test/resources/core-site-security.xml");
         runner.setProperty(KERBEROS_CREDENTIALS_SERVICE, "kcs");
         runner.enableControllerService(kcs);
         runner.assertNotValid();
@@ -1321,58 +1320,4 @@ public class TestPutHive3Streaming {
             return null;
         }
     }
-
-    private static class MockKerberosCredentialsService implements KerberosCredentialsService, ControllerService {
-
-        private String keytab = "src/test/resources/fake.keytab";
-        private String principal = "test@REALM.COM";
-
-        public MockKerberosCredentialsService() {
-        }
-
-        public MockKerberosCredentialsService(String keytab, String principal) {
-            this.keytab = keytab;
-            this.principal = principal;
-        }
-
-        @Override
-        public String getKeytab() {
-            return keytab;
-        }
-
-        @Override
-        public String getPrincipal() {
-            return principal;
-        }
-
-        @Override
-        public void initialize(ControllerServiceInitializationContext context) throws InitializationException {
-
-        }
-
-        @Override
-        public Collection<ValidationResult> validate(ValidationContext context) {
-            return Collections.EMPTY_LIST;
-        }
-
-        @Override
-        public PropertyDescriptor getPropertyDescriptor(String name) {
-            return null;
-        }
-
-        @Override
-        public void onPropertyModified(PropertyDescriptor descriptor, String oldValue, String newValue) {
-
-        }
-
-        @Override
-        public List<PropertyDescriptor> getPropertyDescriptors() {
-            return null;
-        }
-
-        @Override
-        public String getIdentifier() {
-            return "kcs";
-        }
-    }
 }
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
index 80e0feb..83ddffc 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
@@ -115,8 +115,27 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.nifi</groupId>
+            <artifactId>nifi-kerberos-test-utils</artifactId>
+            <version>1.13.0-SNAPSHOT</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.hamcrest</groupId>
             <artifactId>hamcrest-all</artifactId>
         </dependency>
     </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.rat</groupId>
+                <artifactId>apache-rat-plugin</artifactId>
+                <configuration>
+                    <excludes combine.children="append">
+                        <exclude>src/test/resources/fake.keytab</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
 </project>
\ No newline at end of file
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
index 786f9c3..de64a36 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/main/java/org/apache/nifi/dbcp/DBCPConnectionPool.java
@@ -513,6 +513,15 @@ public class DBCPConnectionPool extends AbstractControllerService implements DBC
             }
             return con;
         } catch (final SQLException e) {
+            // If using Kerberos,  attempt to re-login
+            if (kerberosUser != null) {
+                try {
+                    getLogger().info("Error getting connection, performing Kerberos re-login");
+                    kerberosUser.login();
+                } catch (LoginException le) {
+                    throw new ProcessException("Unable to authenticate Kerberos principal", le);
+                }
+            }
             throw new ProcessException(e);
         }
     }
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
index 6071474..373d13e 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/java/org/apache/nifi/dbcp/DBCPServiceTest.java
@@ -17,6 +17,8 @@
 package org.apache.nifi.dbcp;
 
 import org.apache.derby.drda.NetworkServerControl;
+import org.apache.nifi.kerberos.KerberosCredentialsService;
+import org.apache.nifi.kerberos.MockKerberosCredentialsService;
 import org.apache.nifi.processor.exception.ProcessException;
 import org.apache.nifi.reporting.InitializationException;
 import org.apache.nifi.util.TestRunner;
@@ -304,6 +306,42 @@ public class DBCPServiceTest {
         connection.close(); // return to pool
     }
 
+    /**
+     * Test that we relogin to Kerberos if a ConnectException occurs during getConnection().
+     */
+    @Test(expected = ProcessException.class)
+    public void testConnectExceptionCausesKerberosRelogin() throws InitializationException, SQLException {
+        final TestRunner runner = TestRunners.newTestRunner(TestProcessor.class);
+        final DBCPConnectionPool service = new DBCPConnectionPool();
+        runner.addControllerService("test-good1", service);
+
+        final KerberosCredentialsService kerberosCredentialsService = new MockKerberosCredentialsService();
+        runner.addControllerService("kcs", kerberosCredentialsService);
+        runner.setProperty(kerberosCredentialsService, MockKerberosCredentialsService.PRINCIPAL, "bad@PRINCIPAL.COM");
+        runner.setProperty(kerberosCredentialsService, MockKerberosCredentialsService.KEYTAB, "src/test/resources/fake.keytab");
+        runner.enableControllerService(kerberosCredentialsService);
+
+        // set fake Derby database connection url
+        runner.setProperty(service, DBCPConnectionPool.DATABASE_URL, "jdbc:derby://localhost:1527/NoDB");
+        runner.setProperty(service, DBCPConnectionPool.DB_USER, "tester");
+        runner.setProperty(service, DBCPConnectionPool.DB_PASSWORD, "testerp");
+        // Use the client driver here rather than the embedded one, as it will generate a ConnectException for the test
+        runner.setProperty(service, DBCPConnectionPool.DB_DRIVERNAME, "org.apache.derby.jdbc.ClientDriver");
+        runner.setProperty(service, DBCPConnectionPool.KERBEROS_CREDENTIALS_SERVICE, "kcs");
+
+        try {
+            runner.enableControllerService(service);
+        } catch (AssertionError ae) {
+            // Ignore, this happens because it tries to do the initial Kerberos login
+        }
+
+        runner.assertValid(service);
+        final DBCPService dbcpService = (DBCPService) runner.getProcessContext().getControllerServiceLookup().getControllerService("test-good1");
+        Assert.assertNotNull(dbcpService);
+
+        dbcpService.getConnection();
+    }
+
     @Rule
     public ExpectedException exception = ExpectedException.none();
 
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/resources/fake.keytab b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/src/test/resources/fake.keytab
new file mode 100644
index 0000000..e69de29


[nifi] 05/11: NIFI-7803: Allow ExecuteSQL(Record) properties to evaluate flowfile attributes where possible

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 6aa58d1374129f723dc5130ab88da10d1f755c94
Author: Matthew Burgess <ma...@apache.org>
AuthorDate: Fri Sep 11 14:19:38 2020 -0400

    NIFI-7803: Allow ExecuteSQL(Record) properties to evaluate flowfile attributes where possible
    
    Signed-off-by: Pierre Villard <pi...@gmail.com>
    
    This closes #4523.
---
 .../nifi/processors/standard/AbstractExecuteSQL.java      | 15 ++++++++-------
 .../org/apache/nifi/processors/standard/ExecuteSQL.java   |  2 +-
 .../apache/nifi/processors/standard/ExecuteSQLRecord.java |  2 +-
 .../apache/nifi/processors/standard/TestExecuteSQL.java   |  6 ++++--
 4 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/AbstractExecuteSQL.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/AbstractExecuteSQL.java
index eeee845..6481181 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/AbstractExecuteSQL.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/AbstractExecuteSQL.java
@@ -127,6 +127,7 @@ public abstract class AbstractExecuteSQL extends AbstractProcessor {
             .defaultValue("0 seconds")
             .required(true)
             .addValidator(StandardValidators.TIME_PERIOD_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
             .sensitive(false)
             .build();
 
@@ -138,7 +139,7 @@ public abstract class AbstractExecuteSQL extends AbstractProcessor {
             .defaultValue("0")
             .required(true)
             .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
-            .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
             .build();
 
     public static final PropertyDescriptor OUTPUT_BATCH_SIZE = new PropertyDescriptor.Builder()
@@ -152,7 +153,7 @@ public abstract class AbstractExecuteSQL extends AbstractProcessor {
             .defaultValue("0")
             .required(true)
             .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
-            .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
             .build();
 
     public static final PropertyDescriptor FETCH_SIZE = new PropertyDescriptor.Builder()
@@ -163,7 +164,7 @@ public abstract class AbstractExecuteSQL extends AbstractProcessor {
             .defaultValue("0")
             .required(true)
             .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
-            .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+            .expressionLanguageSupported(ExpressionLanguageScope.FLOWFILE_ATTRIBUTES)
             .build();
 
     protected List<PropertyDescriptor> propDescriptors;
@@ -210,11 +211,11 @@ public abstract class AbstractExecuteSQL extends AbstractProcessor {
         final List<FlowFile> resultSetFlowFiles = new ArrayList<>();
 
         final ComponentLog logger = getLogger();
-        final Integer queryTimeout = context.getProperty(QUERY_TIMEOUT).asTimePeriod(TimeUnit.SECONDS).intValue();
-        final Integer maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions().asInteger();
-        final Integer outputBatchSizeField = context.getProperty(OUTPUT_BATCH_SIZE).evaluateAttributeExpressions().asInteger();
+        final int queryTimeout = context.getProperty(QUERY_TIMEOUT).evaluateAttributeExpressions(fileToProcess).asTimePeriod(TimeUnit.SECONDS).intValue();
+        final Integer maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions(fileToProcess).asInteger();
+        final Integer outputBatchSizeField = context.getProperty(OUTPUT_BATCH_SIZE).evaluateAttributeExpressions(fileToProcess).asInteger();
         final int outputBatchSize = outputBatchSizeField == null ? 0 : outputBatchSizeField;
-        final Integer fetchSize = context.getProperty(FETCH_SIZE).evaluateAttributeExpressions().asInteger();
+        final Integer fetchSize = context.getProperty(FETCH_SIZE).evaluateAttributeExpressions(fileToProcess).asInteger();
 
         List<String> preQueries = getQueries(context.getProperty(SQL_PRE_QUERY).evaluateAttributeExpressions(fileToProcess).getValue());
         List<String> postQueries = getQueries(context.getProperty(SQL_POST_QUERY).evaluateAttributeExpressions(fileToProcess).getValue());
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQL.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQL.java
index 240075f..c1b8161 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQL.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQL.java
@@ -141,7 +141,7 @@ public class ExecuteSQL extends AbstractExecuteSQL {
     protected SqlWriter configureSqlWriter(ProcessSession session, ProcessContext context, FlowFile fileToProcess) {
         final boolean convertNamesForAvro = context.getProperty(NORMALIZE_NAMES_FOR_AVRO).asBoolean();
         final Boolean useAvroLogicalTypes = context.getProperty(USE_AVRO_LOGICAL_TYPES).asBoolean();
-        final Integer maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions().asInteger();
+        final Integer maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions(fileToProcess).asInteger();
         final Integer defaultPrecision = context.getProperty(DEFAULT_PRECISION).evaluateAttributeExpressions(fileToProcess).asInteger();
         final Integer defaultScale = context.getProperty(DEFAULT_SCALE).evaluateAttributeExpressions(fileToProcess).asInteger();
         final String codec = context.getProperty(COMPRESSION_FORMAT).getValue();
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQLRecord.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQLRecord.java
index 3e3dbea..70c4037 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQLRecord.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/ExecuteSQLRecord.java
@@ -141,7 +141,7 @@ public class ExecuteSQLRecord extends AbstractExecuteSQL {
 
     @Override
     protected SqlWriter configureSqlWriter(ProcessSession session, ProcessContext context, FlowFile fileToProcess) {
-        final Integer maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions().asInteger();
+        final Integer maxRowsPerFlowFile = context.getProperty(MAX_ROWS_PER_FLOW_FILE).evaluateAttributeExpressions(fileToProcess).asInteger();
         final boolean convertNamesForAvro = context.getProperty(NORMALIZE_NAMES).asBoolean();
         final Boolean useAvroLogicalTypes = context.getProperty(USE_AVRO_LOGICAL_TYPES).asBoolean();
         final JdbcCommon.AvroConversionOptions options = JdbcCommon.AvroConversionOptions.builder()
diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestExecuteSQL.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestExecuteSQL.java
index 0c6da65..16cd1d5 100644
--- a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestExecuteSQL.java
+++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/test/java/org/apache/nifi/processors/standard/TestExecuteSQL.java
@@ -309,9 +309,11 @@ public class TestExecuteSQL {
         String testAttrName = "attr1";
         String testAttrValue = "value1";
         attrMap.put(testAttrName, testAttrValue);
+        attrMap.put("max.rows", "5");
+        attrMap.put("batch.size", "1");
         runner.setIncomingConnection(true);
-        runner.setProperty(ExecuteSQL.MAX_ROWS_PER_FLOW_FILE, "5");
-        runner.setProperty(ExecuteSQL.OUTPUT_BATCH_SIZE, "1");
+        runner.setProperty(ExecuteSQL.MAX_ROWS_PER_FLOW_FILE, "${max.rows}");
+        runner.setProperty(ExecuteSQL.OUTPUT_BATCH_SIZE, "${batch.size}");
         MockFlowFile inputFlowFile = runner.enqueue("SELECT * FROM TEST_NULL_INT", attrMap);
         runner.run();
 


[nifi] 08/11: NIFI-5583: Add cdc processor for MySQL referring to GTID.

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 174720104872b43dbaf94a268690d986de7f6b03
Author: yoshiata <yo...@yahoo-corp.jp>
AuthorDate: Mon Jul 2 15:55:50 2018 +0900

    NIFI-5583: Add cdc processor for MySQL referring to GTID.
    
    Signed-off-by: Pierre Villard <pi...@gmail.com>
    
    This closes #2997.
---
 .../nifi/cdc/mysql/event/BaseBinlogEventInfo.java  |  10 ++
 .../cdc/mysql/event/BaseBinlogRowEventInfo.java    |   6 +
 .../cdc/mysql/event/BaseBinlogTableEventInfo.java  |   5 +
 .../cdc/mysql/event/BeginTransactionEventInfo.java |   5 +
 .../nifi/cdc/mysql/event/BinlogEventInfo.java      |   3 +
 .../mysql/event/CommitTransactionEventInfo.java    |   5 +
 .../apache/nifi/cdc/mysql/event/DDLEventInfo.java  |   5 +
 .../nifi/cdc/mysql/event/DeleteRowsEventInfo.java  |   4 +
 .../nifi/cdc/mysql/event/InsertRowsEventInfo.java  |   5 +
 .../nifi/cdc/mysql/event/UpdateRowsEventInfo.java  |   5 +
 .../mysql/event/io/AbstractBinlogEventWriter.java  |  19 +-
 .../cdc/mysql/processors/CaptureChangeMySQL.java   | 177 ++++++++++++++-----
 .../mysql/processors/CaptureChangeMySQLTest.groovy | 193 +++++++++++++++++++++
 13 files changed, 393 insertions(+), 49 deletions(-)

diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogEventInfo.java
index 7089cda..9106e93 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogEventInfo.java
@@ -25,6 +25,7 @@ public class BaseBinlogEventInfo extends BaseEventInfo implements BinlogEventInf
 
     private String binlogFilename;
     private Long binlogPosition;
+    private String binlogGtidSet;
 
     public BaseBinlogEventInfo(String eventType, Long timestamp, String binlogFilename, Long binlogPosition) {
         super(eventType, timestamp);
@@ -32,6 +33,11 @@ public class BaseBinlogEventInfo extends BaseEventInfo implements BinlogEventInf
         this.binlogPosition = binlogPosition;
     }
 
+    public BaseBinlogEventInfo(String eventType, Long timestamp, String binlogGtidSet) {
+        super(eventType, timestamp);
+        this.binlogGtidSet = binlogGtidSet;
+    }
+
     public String getBinlogFilename() {
         return binlogFilename;
     }
@@ -39,4 +45,8 @@ public class BaseBinlogEventInfo extends BaseEventInfo implements BinlogEventInf
     public Long getBinlogPosition() {
         return binlogPosition;
     }
+
+    public String getBinlogGtidSet() {
+        return binlogGtidSet;
+    }
 }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogRowEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogRowEventInfo.java
index 4796947..6517225 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogRowEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogRowEventInfo.java
@@ -37,6 +37,12 @@ public class BaseBinlogRowEventInfo<RowEventDataType> extends BaseBinlogTableEve
         this.delegate = new BaseRowEventInfo<>(tableInfo, type, timestamp, rows);
     }
 
+    public BaseBinlogRowEventInfo(TableInfo tableInfo, String type, Long timestamp, String binlogGtidSet, BitSet includedColumns, List<RowEventDataType> rows) {
+        super(tableInfo, type, timestamp, binlogGtidSet);
+        this.includedColumns = includedColumns;
+        this.delegate = new BaseRowEventInfo<>(tableInfo, type, timestamp, rows);
+    }
+
     public BitSet getIncludedColumns() {
         return includedColumns;
     }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogTableEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogTableEventInfo.java
index c5d3ddf..ea23023 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogTableEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BaseBinlogTableEventInfo.java
@@ -35,6 +35,11 @@ public class BaseBinlogTableEventInfo extends BaseBinlogEventInfo implements Bin
         this.delegate = new BaseTableEventInfo(tableInfo, DDL_EVENT, timestamp);
     }
 
+    public BaseBinlogTableEventInfo(TableInfo tableInfo, String eventType, Long timestamp, String binlogGtidSet) {
+        super(eventType, timestamp, binlogGtidSet);
+        this.delegate = new BaseTableEventInfo(tableInfo, DDL_EVENT, timestamp);
+    }
+
     @Override
     public String getDatabaseName() {
         return delegate.getDatabaseName();
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BeginTransactionEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BeginTransactionEventInfo.java
index c6f5af0..ed899f5 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BeginTransactionEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BeginTransactionEventInfo.java
@@ -28,6 +28,11 @@ public class BeginTransactionEventInfo extends BaseBinlogEventInfo {
         this.databaseName = databaseName;
     }
 
+    public BeginTransactionEventInfo(String databaseName, Long timestamp, String binlogGtidSet) {
+        super(BEGIN_EVENT, timestamp, binlogGtidSet);
+        this.databaseName = databaseName;
+    }
+
     public String getDatabaseName() {
         return databaseName;
     }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BinlogEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BinlogEventInfo.java
index 6398d4f..9784fc4 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BinlogEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/BinlogEventInfo.java
@@ -25,8 +25,11 @@ public interface BinlogEventInfo extends EventInfo {
 
     String BINLOG_FILENAME_KEY = "binlog.filename";
     String BINLOG_POSITION_KEY = "binlog.position";
+    String BINLOG_GTIDSET_KEY = "binlog.gtidset";
 
     String getBinlogFilename();
 
     Long getBinlogPosition();
+
+    String getBinlogGtidSet();
 }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/CommitTransactionEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/CommitTransactionEventInfo.java
index b4a230a..a5406ea 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/CommitTransactionEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/CommitTransactionEventInfo.java
@@ -29,6 +29,11 @@ public class CommitTransactionEventInfo extends BaseBinlogEventInfo {
         this.databaseName = databaseName;
     }
 
+    public CommitTransactionEventInfo(String databaseName, Long timestamp, String binlogGtidSet) {
+        super(COMMIT_EVENT, timestamp, binlogGtidSet);
+        this.databaseName = databaseName;
+    }
+
     public String getDatabaseName() {
         return databaseName;
     }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DDLEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DDLEventInfo.java
index bc2c871..6da8d80 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DDLEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DDLEventInfo.java
@@ -32,6 +32,11 @@ public class DDLEventInfo extends BaseBinlogTableEventInfo implements TableEvent
         this.query = query;
     }
 
+    public DDLEventInfo(TableInfo tableInfo, Long timestamp, String binlogGtidSet, String query) {
+        super(tableInfo, DDL_EVENT, timestamp, binlogGtidSet);
+        this.query = query;
+    }
+
     public String getQuery() {
         return query;
     }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DeleteRowsEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DeleteRowsEventInfo.java
index 3b538ec..98db096 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DeleteRowsEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/DeleteRowsEventInfo.java
@@ -29,4 +29,8 @@ public class DeleteRowsEventInfo extends BaseBinlogRowEventInfo<Serializable[]>
     public DeleteRowsEventInfo(TableInfo tableInfo, Long timestamp, String binlogFilename, Long binlogPosition, DeleteRowsEventData data) {
         super(tableInfo, DELETE_EVENT, timestamp, binlogFilename, binlogPosition, data.getIncludedColumns(), data.getRows());
     }
+
+    public DeleteRowsEventInfo(TableInfo tableInfo, Long timestamp, String binlogGtidSet, DeleteRowsEventData data) {
+        super(tableInfo, DELETE_EVENT, timestamp, binlogGtidSet, data.getIncludedColumns(), data.getRows());
+    }
 }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/InsertRowsEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/InsertRowsEventInfo.java
index 7d75511..edce548 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/InsertRowsEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/InsertRowsEventInfo.java
@@ -32,4 +32,9 @@ public class InsertRowsEventInfo extends BaseBinlogRowEventInfo<Serializable[]>
         super(tableInfo, INSERT_EVENT, timestamp, binlogFilename, binlogPosition, data.getIncludedColumns(), data.getRows());
         this.data = data;
     }
+
+    public InsertRowsEventInfo(TableInfo tableInfo, Long timestamp, String binlogGtidSet, WriteRowsEventData data) {
+        super(tableInfo, INSERT_EVENT, timestamp, binlogGtidSet, data.getIncludedColumns(), data.getRows());
+        this.data = data;
+    }
 }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/UpdateRowsEventInfo.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/UpdateRowsEventInfo.java
index 2e20dc6..14edcaa 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/UpdateRowsEventInfo.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/UpdateRowsEventInfo.java
@@ -35,6 +35,11 @@ public class UpdateRowsEventInfo extends BaseBinlogRowEventInfo<Map.Entry<Serial
         includedColumnsBeforeUpdate = data.getIncludedColumnsBeforeUpdate();
     }
 
+    public UpdateRowsEventInfo(TableInfo tableInfo, Long timestamp, String binlogGtidSet, UpdateRowsEventData data) {
+        super(tableInfo, UPDATE_EVENT, timestamp, binlogGtidSet, data.getIncludedColumns(), data.getRows());
+        includedColumnsBeforeUpdate = data.getIncludedColumnsBeforeUpdate();
+    }
+
     public BitSet getIncludedColumnsBeforeUpdate() {
         return includedColumnsBeforeUpdate;
     }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/io/AbstractBinlogEventWriter.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/io/AbstractBinlogEventWriter.java
index df4424c..09186f2 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/io/AbstractBinlogEventWriter.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/event/io/AbstractBinlogEventWriter.java
@@ -33,8 +33,14 @@ import java.util.Map;
 public abstract class AbstractBinlogEventWriter<T extends BinlogEventInfo> extends AbstractEventWriter<T> {
 
     protected void writeJson(T event) throws IOException {
-        jsonGenerator.writeStringField("binlog_filename", event.getBinlogFilename());
-        jsonGenerator.writeNumberField("binlog_position", event.getBinlogPosition());
+        String gtidSet = event.getBinlogGtidSet();
+
+        if (gtidSet == null) {
+            jsonGenerator.writeStringField("binlog_filename", event.getBinlogFilename());
+            jsonGenerator.writeNumberField("binlog_position", event.getBinlogPosition());
+        } else {
+            jsonGenerator.writeStringField("binlog_gtidset", event.getBinlogGtidSet());
+        }
     }
 
     protected Map<String, String> getCommonAttributes(final long sequenceId, BinlogEventInfo eventInfo) {
@@ -42,8 +48,13 @@ public abstract class AbstractBinlogEventWriter<T extends BinlogEventInfo> exten
             {
                 put(SEQUENCE_ID_KEY, Long.toString(sequenceId));
                 put(CDC_EVENT_TYPE_ATTRIBUTE, eventInfo.getEventType());
-                put(BinlogEventInfo.BINLOG_FILENAME_KEY, eventInfo.getBinlogFilename());
-                put(BinlogEventInfo.BINLOG_POSITION_KEY, Long.toString(eventInfo.getBinlogPosition()));
+                String gtidSet = eventInfo.getBinlogGtidSet();
+                if (gtidSet == null) {
+                    put(BinlogEventInfo.BINLOG_FILENAME_KEY, eventInfo.getBinlogFilename());
+                    put(BinlogEventInfo.BINLOG_POSITION_KEY, Long.toString(eventInfo.getBinlogPosition()));
+                } else {
+                    put(BinlogEventInfo.BINLOG_GTIDSET_KEY, gtidSet);
+                }
                 put(CoreAttributes.MIME_TYPE.key(), APPLICATION_JSON);
             }
         };
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java
index 1fcbfa6..edeb49c 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/main/java/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQL.java
@@ -26,9 +26,11 @@ import static com.github.shyiko.mysql.binlog.event.EventType.ROTATE;
 import static com.github.shyiko.mysql.binlog.event.EventType.WRITE_ROWS;
 
 import com.github.shyiko.mysql.binlog.BinaryLogClient;
+import com.github.shyiko.mysql.binlog.GtidSet;
 import com.github.shyiko.mysql.binlog.event.Event;
 import com.github.shyiko.mysql.binlog.event.EventHeaderV4;
 import com.github.shyiko.mysql.binlog.event.EventType;
+import com.github.shyiko.mysql.binlog.event.GtidEventData;
 import com.github.shyiko.mysql.binlog.event.QueryEventData;
 import com.github.shyiko.mysql.binlog.event.RotateEventData;
 import com.github.shyiko.mysql.binlog.event.TableMapEventData;
@@ -314,7 +316,8 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             .name("capture-change-mysql-init-binlog-filename")
             .displayName("Initial Binlog Filename")
             .description("Specifies an initial binlog filename to use if this processor's State does not have a current binlog filename. If a filename is present "
-                    + "in the processor's State, this property is ignored. This can be used along with Initial Binlog Position to \"skip ahead\" if previous events are not desired. "
+                    + "in the processor's State or \"Use GTID\" property is set to false, this property is ignored. "
+                    + "This can be used along with Initial Binlog Position to \"skip ahead\" if previous events are not desired. "
                     + "Note that NiFi Expression Language is supported, but this property is evaluated when the processor is configured, so FlowFile attributes may not be used. Expression "
                     + "Language is supported to enable the use of the Variable Registry and/or environment properties.")
             .required(false)
@@ -326,30 +329,57 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             .name("capture-change-mysql-init-binlog-position")
             .displayName("Initial Binlog Position")
             .description("Specifies an initial offset into a binlog (specified by Initial Binlog Filename) to use if this processor's State does not have a current "
-                    + "binlog filename. If a filename is present in the processor's State, this property is ignored. This can be used along with Initial Binlog Filename "
-                    + "to \"skip ahead\" if previous events are not desired. Note that NiFi Expression Language is supported, but this property is evaluated when the "
-                    + "processor is configured, so FlowFile attributes may not be used. Expression Language is supported to enable the use of the Variable Registry "
-                    + "and/or environment properties.")
+                    + "binlog filename. If a filename is present in the processor's State or \"Use GTID\" property is false, this property is ignored. "
+                    + "This can be used along with Initial Binlog Filename to \"skip ahead\" if previous events are not desired. Note that NiFi Expression Language "
+                    + "is supported, but this property is evaluated when the processor is configured, so FlowFile attributes may not be used. Expression Language is "
+                    + "supported to enable the use of the Variable Registry and/or environment properties.")
             .required(false)
             .addValidator(StandardValidators.NON_NEGATIVE_INTEGER_VALIDATOR)
             .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
             .build();
 
+    public static final PropertyDescriptor USE_BINLOG_GTID = new PropertyDescriptor.Builder()
+            .name("capture-change-mysql-use-gtid")
+            .displayName("Use Binlog GTID")
+            .description("Specifies whether to use Global Transaction ID (GTID) for binlog tracking. If set to true, processor's state of binlog file name and position is ignored. "
+                    + "The main benefit of using GTID is to have much reliable failover than using binlog filename/position.")
+            .required(true)
+            .allowableValues("true", "false")
+            .defaultValue("false")
+            .addValidator(StandardValidators.BOOLEAN_VALIDATOR)
+            .build();
+
+    public static final PropertyDescriptor INIT_BINLOG_GTID = new PropertyDescriptor.Builder()
+            .name("capture-change-mysql-init-gtid")
+            .displayName("Initial Binlog GTID")
+            .description("Specifies an initial GTID to use if this processor's State does not have a current GTID. "
+                    + "If a GTID is present in the processor's State or \"Use GTID\" property is set to false, this property is ignored. "
+                    + "This can be used to \"skip ahead\" if previous events are not desired. "
+                    + "Note that NiFi Expression Language is supported, but this property is evaluated when the processor is configured, so FlowFile attributes may not be used. "
+                    + "Expression Language is supported to enable the use of the Variable Registry and/or environment properties.")
+            .required(false)
+            .addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
+            .expressionLanguageSupported(ExpressionLanguageScope.VARIABLE_REGISTRY)
+            .build();
+
     private static final List<PropertyDescriptor> propDescriptors;
 
     private volatile ProcessSession currentSession;
     private BinaryLogClient binlogClient;
     private BinlogEventListener eventListener;
     private BinlogLifecycleListener lifecycleListener;
+    private GtidSet gtidSet;
 
     private final LinkedBlockingQueue<RawBinlogEvent> queue = new LinkedBlockingQueue<>();
     private volatile String currentBinlogFile = null;
     private volatile long currentBinlogPosition = 4;
+    private volatile String currentGtidSet = null;
 
-    // The following variables save the value of the binlog filename and position (and sequence id) at the beginning of a transaction. Used for rollback
+    // The following variables save the value of the binlog filename, position, (sequence id), and gtid at the beginning of a transaction. Used for rollback
     private volatile String xactBinlogFile = null;
     private volatile long xactBinlogPosition = 4;
     private volatile long xactSequenceId = 0;
+    private volatile String xactGtidSet = null;
 
     private volatile TableInfo currentTable = null;
     private volatile String currentDatabase = null;
@@ -357,6 +387,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
     private volatile Pattern tableNamePattern;
     private volatile boolean includeBeginCommit = false;
     private volatile boolean includeDDLEvents = false;
+    private volatile boolean useGtid = false;
 
     private volatile boolean inTransaction = false;
     private volatile boolean skipTable = false;
@@ -408,6 +439,8 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
         pds.add(INIT_SEQUENCE_ID);
         pds.add(INIT_BINLOG_FILENAME);
         pds.add(INIT_BINLOG_POSITION);
+        pds.add(USE_BINLOG_GTID);
+        pds.add(INIT_BINLOG_GTID);
         propDescriptors = Collections.unmodifiableList(pds);
     }
 
@@ -450,32 +483,48 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
 
         includeBeginCommit = context.getProperty(INCLUDE_BEGIN_COMMIT).asBoolean();
         includeDDLEvents = context.getProperty(INCLUDE_DDL_EVENTS).asBoolean();
-
-        // Set current binlog filename to whatever is in State, falling back to the Retrieve All Records then Initial Binlog Filename if no State variable is present
-        currentBinlogFile = stateMap.get(BinlogEventInfo.BINLOG_FILENAME_KEY);
-        if (currentBinlogFile == null) {
-            if (!getAllRecords) {
-                if (context.getProperty(INIT_BINLOG_FILENAME).isSet()) {
-                    currentBinlogFile = context.getProperty(INIT_BINLOG_FILENAME).evaluateAttributeExpressions().getValue();
+        useGtid = context.getProperty(USE_BINLOG_GTID).asBoolean();
+
+        if (useGtid) {
+            // Set current gtid to whatever is in State, falling back to the Retrieve All Records then Initial Gtid if no State variable is present
+            currentGtidSet = stateMap.get(BinlogEventInfo.BINLOG_GTIDSET_KEY);
+            if (currentGtidSet == null) {
+                if (!getAllRecords && context.getProperty(INIT_BINLOG_GTID).isSet()) {
+                    currentGtidSet = context.getProperty(INIT_BINLOG_GTID).evaluateAttributeExpressions().getValue();
+                } else {
+                    // If we're starting from the beginning of all binlogs, the binlog gtid must be the empty string (not null)
+                    currentGtidSet = "";
+                }
+            }
+            currentBinlogFile = "";
+            currentBinlogPosition = DO_NOT_SET;
+        } else {
+            // Set current binlog filename to whatever is in State, falling back to the Retrieve All Records then Initial Binlog Filename if no State variable is present
+            currentBinlogFile = stateMap.get(BinlogEventInfo.BINLOG_FILENAME_KEY);
+            if (currentBinlogFile == null) {
+                if (!getAllRecords) {
+                    if (context.getProperty(INIT_BINLOG_FILENAME).isSet()) {
+                        currentBinlogFile = context.getProperty(INIT_BINLOG_FILENAME).evaluateAttributeExpressions().getValue();
+                    }
+                } else {
+                    // If we're starting from the beginning of all binlogs, the binlog filename must be the empty string (not null)
+                    currentBinlogFile = "";
                 }
-            } else {
-                // If we're starting from the beginning of all binlogs, the binlog filename must be the empty string (not null)
-                currentBinlogFile = "";
             }
-        }
 
-        // Set current binlog position to whatever is in State, falling back to the Retrieve All Records then Initial Binlog Filename if no State variable is present
-        String binlogPosition = stateMap.get(BinlogEventInfo.BINLOG_POSITION_KEY);
-        if (binlogPosition != null) {
-            currentBinlogPosition = Long.valueOf(binlogPosition);
-        } else if (!getAllRecords) {
-            if (context.getProperty(INIT_BINLOG_POSITION).isSet()) {
-                currentBinlogPosition = context.getProperty(INIT_BINLOG_POSITION).evaluateAttributeExpressions().asLong();
+            // Set current binlog position to whatever is in State, falling back to the Retrieve All Records then Initial Binlog Filename if no State variable is present
+            String binlogPosition = stateMap.get(BinlogEventInfo.BINLOG_POSITION_KEY);
+            if (binlogPosition != null) {
+                currentBinlogPosition = Long.valueOf(binlogPosition);
+            } else if (!getAllRecords) {
+                if (context.getProperty(INIT_BINLOG_POSITION).isSet()) {
+                    currentBinlogPosition = context.getProperty(INIT_BINLOG_POSITION).evaluateAttributeExpressions().asLong();
+                } else {
+                    currentBinlogPosition = DO_NOT_SET;
+                }
             } else {
-                currentBinlogPosition = DO_NOT_SET;
+                currentBinlogPosition = -1;
             }
-        } else {
-            currentBinlogPosition = -1;
         }
 
         // Get current sequence ID from state
@@ -571,7 +620,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             long timeSinceLastUpdate = now - lastStateUpdate;
 
             if (stateUpdateInterval != 0 && timeSinceLastUpdate >= stateUpdateInterval) {
-                updateState(stateManager, currentBinlogFile, currentBinlogPosition, currentSequenceId.get());
+                updateState(stateManager, currentBinlogFile, currentBinlogPosition, currentSequenceId.get(), currentGtidSet);
                 lastStateUpdate = now;
             }
         } catch (IOException ioe) {
@@ -580,6 +629,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                 currentBinlogFile = xactBinlogFile == null ? "" : xactBinlogFile;
                 currentBinlogPosition = xactBinlogPosition;
                 currentSequenceId.set(xactSequenceId);
+                currentGtidSet = xactGtidSet;
                 inTransaction = false;
                 stop(stateManager);
                 queue.clear();
@@ -681,6 +731,9 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                 binlogClient.setBinlogPosition(currentBinlogPosition);
             }
 
+            binlogClient.setGtidSet(currentGtidSet);
+            binlogClient.setGtidSetFallbackToPurged(true);
+
             if (serverId != null) {
                 binlogClient.setServerId(serverId);
             }
@@ -719,6 +772,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             }
         }
 
+        gtidSet = new GtidSet(binlogClient.getGtidSet());
         doStop.set(false);
     }
 
@@ -736,7 +790,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             // We always get ROTATE and FORMAT_DESCRIPTION messages no matter where we start (even from the end), and they won't have the correct "next position" value, so only
             // advance the position if it is not that type of event. ROTATE events don't generate output CDC events and have the current binlog position in a special field, which
             // is filled in during the ROTATE case
-            if (eventType != ROTATE && eventType != FORMAT_DESCRIPTION) {
+            if (eventType != ROTATE && eventType != FORMAT_DESCRIPTION && !useGtid) {
                 currentBinlogPosition = header.getPosition();
             }
             log.debug("Got message event type: {} ", new Object[]{header.getEventType().toString()});
@@ -790,13 +844,16 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                         if (inTransaction) {
                             throw new IOException("BEGIN event received while already processing a transaction. This could indicate that your binlog position is invalid.");
                         }
-                        // Mark the current binlog position in case we have to rollback the transaction (if the processor is stopped, e.g.)
+                        // Mark the current binlog position and GTID in case we have to rollback the transaction (if the processor is stopped, e.g.)
                         xactBinlogFile = currentBinlogFile;
                         xactBinlogPosition = currentBinlogPosition;
                         xactSequenceId = currentSequenceId.get();
+                        xactGtidSet = currentGtidSet;
 
                         if (includeBeginCommit && (databaseNamePattern == null || databaseNamePattern.matcher(currentDatabase).matches())) {
-                            BeginTransactionEventInfo beginEvent = new BeginTransactionEventInfo(currentDatabase, timestamp, currentBinlogFile, currentBinlogPosition);
+                            BeginTransactionEventInfo beginEvent = useGtid
+                                    ? new BeginTransactionEventInfo(currentDatabase, timestamp, currentGtidSet)
+                                    : new BeginTransactionEventInfo(currentDatabase, timestamp, currentBinlogFile, currentBinlogPosition);
                             currentSequenceId.set(beginEventWriter.writeEvent(currentSession, transitUri, beginEvent, currentSequenceId.get(), REL_SUCCESS));
                         }
                         inTransaction = true;
@@ -807,7 +864,9 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                         }
                         // InnoDB generates XID events for "commit", but MyISAM generates Query events with "COMMIT", so handle that here
                         if (includeBeginCommit && (databaseNamePattern == null || databaseNamePattern.matcher(currentDatabase).matches())) {
-                            CommitTransactionEventInfo commitTransactionEvent = new CommitTransactionEventInfo(currentDatabase, timestamp, currentBinlogFile, currentBinlogPosition);
+                            CommitTransactionEventInfo commitTransactionEvent = useGtid
+                                    ? new CommitTransactionEventInfo(currentDatabase, timestamp, currentGtidSet)
+                                    : new CommitTransactionEventInfo(currentDatabase, timestamp, currentBinlogFile, currentBinlogPosition);
                             currentSequenceId.set(commitEventWriter.writeEvent(currentSession, transitUri, commitTransactionEvent, currentSequenceId.get(), REL_SUCCESS));
                         }
                         // Commit the NiFi session
@@ -829,7 +888,9 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                             if (includeDDLEvents && (databaseNamePattern == null || databaseNamePattern.matcher(currentDatabase).matches())) {
                                 // If we don't have table information, we can still use the database name
                                 TableInfo ddlTableInfo = (currentTable != null) ? currentTable : new TableInfo(currentDatabase, null, null, null);
-                                DDLEventInfo ddlEvent = new DDLEventInfo(ddlTableInfo, timestamp, currentBinlogFile, currentBinlogPosition, sql);
+                                DDLEventInfo ddlEvent = useGtid
+                                        ? new DDLEventInfo(ddlTableInfo, timestamp, currentGtidSet, sql)
+                                        : new DDLEventInfo(ddlTableInfo, timestamp, currentBinlogFile, currentBinlogPosition, sql);
                                 currentSequenceId.set(ddlEventWriter.writeEvent(currentSession, transitUri, ddlEvent, currentSequenceId.get(), REL_SUCCESS));
                             }
                             // Remove all the keys from the cache that this processor added
@@ -850,7 +911,9 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                                 + "This could indicate that your binlog position is invalid.");
                     }
                     if (includeBeginCommit && (databaseNamePattern == null || databaseNamePattern.matcher(currentDatabase).matches())) {
-                        CommitTransactionEventInfo commitTransactionEvent = new CommitTransactionEventInfo(currentDatabase, timestamp, currentBinlogFile, currentBinlogPosition);
+                        CommitTransactionEventInfo commitTransactionEvent = useGtid
+                                ? new CommitTransactionEventInfo(currentDatabase, timestamp, currentGtidSet)
+                                : new CommitTransactionEventInfo(currentDatabase, timestamp, currentBinlogFile, currentBinlogPosition);
                         currentSequenceId.set(commitEventWriter.writeEvent(currentSession, transitUri, commitTransactionEvent, currentSequenceId.get(), REL_SUCCESS));
                     }
                     // Commit the NiFi session
@@ -887,29 +950,47 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
                             || eventType == EXT_WRITE_ROWS
                             || eventType == PRE_GA_WRITE_ROWS) {
 
-                        InsertRowsEventInfo eventInfo = new InsertRowsEventInfo(currentTable, timestamp, currentBinlogFile, currentBinlogPosition, event.getData());
+                        InsertRowsEventInfo eventInfo = useGtid
+                                ? new InsertRowsEventInfo(currentTable, timestamp, currentGtidSet, event.getData())
+                                : new InsertRowsEventInfo(currentTable, timestamp, currentBinlogFile, currentBinlogPosition, event.getData());
                         currentSequenceId.set(insertRowsWriter.writeEvent(currentSession, transitUri, eventInfo, currentSequenceId.get(), REL_SUCCESS));
 
                     } else if (eventType == DELETE_ROWS
                             || eventType == EXT_DELETE_ROWS
                             || eventType == PRE_GA_DELETE_ROWS) {
 
-                        DeleteRowsEventInfo eventInfo = new DeleteRowsEventInfo(currentTable, timestamp, currentBinlogFile, currentBinlogPosition, event.getData());
+                        DeleteRowsEventInfo eventInfo = useGtid
+                                ? new DeleteRowsEventInfo(currentTable, timestamp, currentGtidSet, event.getData())
+                                : new DeleteRowsEventInfo(currentTable, timestamp, currentBinlogFile, currentBinlogPosition, event.getData());
                         currentSequenceId.set(deleteRowsWriter.writeEvent(currentSession, transitUri, eventInfo, currentSequenceId.get(), REL_SUCCESS));
 
                     } else {
                         // Update event
-                        UpdateRowsEventInfo eventInfo = new UpdateRowsEventInfo(currentTable, timestamp, currentBinlogFile, currentBinlogPosition, event.getData());
+                        UpdateRowsEventInfo eventInfo = useGtid
+                                ? new UpdateRowsEventInfo(currentTable, timestamp, currentGtidSet, event.getData())
+                                : new UpdateRowsEventInfo(currentTable, timestamp, currentBinlogFile, currentBinlogPosition, event.getData());
                         currentSequenceId.set(updateRowsWriter.writeEvent(currentSession, transitUri, eventInfo, currentSequenceId.get(), REL_SUCCESS));
                     }
                     break;
 
                 case ROTATE:
-                    // Update current binlog filename
-                    RotateEventData rotateEventData = event.getData();
-                    currentBinlogFile = rotateEventData.getBinlogFilename();
-                    currentBinlogPosition = rotateEventData.getBinlogPosition();
+                    if (!useGtid) {
+                        // Update current binlog filename
+                        RotateEventData rotateEventData = event.getData();
+                        currentBinlogFile = rotateEventData.getBinlogFilename();
+                        currentBinlogPosition = rotateEventData.getBinlogPosition();
+                    }
                     break;
+
+                case GTID:
+                    if (useGtid) {
+                        // Update current binlog gtid
+                        GtidEventData gtidEventData = event.getData();
+                        gtidSet.add(gtidEventData.getGtid());
+                        currentGtidSet = gtidSet.toString();
+                    }
+                    break;
+
                 default:
                     break;
             }
@@ -917,7 +998,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             // Advance the current binlog position. This way if no more events are received and the processor is stopped, it will resume after the event that was just processed.
             // We always get ROTATE and FORMAT_DESCRIPTION messages no matter where we start (even from the end), and they won't have the correct "next position" value, so only
             // advance the position if it is not that type of event.
-            if (eventType != ROTATE && eventType != FORMAT_DESCRIPTION) {
+            if (eventType != ROTATE && eventType != FORMAT_DESCRIPTION && !useGtid) {
                 currentBinlogPosition = header.getNextPosition();
             }
         }
@@ -937,7 +1018,7 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
             doStop.set(true);
 
             if (hasRun.getAndSet(false)) {
-                updateState(stateManager, currentBinlogFile, currentBinlogPosition, currentSequenceId.get());
+                updateState(stateManager, currentBinlogFile, currentBinlogPosition, currentSequenceId.get(), currentGtidSet);
             }
             currentBinlogPosition = -1;
 
@@ -952,17 +1033,23 @@ public class CaptureChangeMySQL extends AbstractSessionFactoryProcessor {
         }
     }
 
-    private void updateState(StateManager stateManager, String binlogFile, long binlogPosition, long sequenceId) throws IOException {
+    private void updateState(StateManager stateManager, String binlogFile, long binlogPosition, long sequenceId, String gtidSet) throws IOException {
         // Update state with latest values
         if (stateManager != null) {
             Map<String, String> newStateMap = new HashMap<>(stateManager.getState(Scope.CLUSTER).toMap());
 
-            // Save current binlog filename and position to the state map
+            // Save current binlog filename, position and GTID to the state map
             if (binlogFile != null) {
                 newStateMap.put(BinlogEventInfo.BINLOG_FILENAME_KEY, binlogFile);
             }
+
             newStateMap.put(BinlogEventInfo.BINLOG_POSITION_KEY, Long.toString(binlogPosition));
             newStateMap.put(EventWriter.SEQUENCE_ID_KEY, String.valueOf(sequenceId));
+
+            if (gtidSet != null) {
+                newStateMap.put(BinlogEventInfo.BINLOG_GTIDSET_KEY, gtidSet);
+            }
+
             stateManager.setState(newStateMap, Scope.CLUSTER);
         }
     }
diff --git a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy
index 5df44f3..bde8479 100644
--- a/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy
+++ b/nifi-nar-bundles/nifi-cdc/nifi-cdc-mysql-bundle/nifi-cdc-mysql-processors/src/test/groovy/org/apache/nifi/cdc/mysql/processors/CaptureChangeMySQLTest.groovy
@@ -22,6 +22,7 @@ import com.github.shyiko.mysql.binlog.event.Event
 import com.github.shyiko.mysql.binlog.event.EventData
 import com.github.shyiko.mysql.binlog.event.EventHeaderV4
 import com.github.shyiko.mysql.binlog.event.EventType
+import com.github.shyiko.mysql.binlog.event.GtidEventData
 import com.github.shyiko.mysql.binlog.event.QueryEventData
 import com.github.shyiko.mysql.binlog.event.RotateEventData
 import com.github.shyiko.mysql.binlog.event.TableMapEventData
@@ -825,11 +826,13 @@ class CaptureChangeMySQLTest {
         // Ensure state not set, as the processor hasn't been stopped and no State Update Interval has been set
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, null, Scope.CLUSTER)
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, null, Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, null, Scope.CLUSTER)
 
         // Stop the processor and verify the state is set
         testRunner.run(1, true, false)
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, 'master.000001', Scope.CLUSTER)
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, '4', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, null, Scope.CLUSTER)
 
         testRunner.stateManager.clear(Scope.CLUSTER)
 
@@ -855,6 +858,7 @@ class CaptureChangeMySQLTest {
 
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, 'master.000001', Scope.CLUSTER)
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, '6', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, null, Scope.CLUSTER)
 
         // COMMIT
         client.sendEvent(new Event(
@@ -866,7 +870,106 @@ class CaptureChangeMySQLTest {
 
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, 'master.000001', Scope.CLUSTER)
         testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, '12', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, null, Scope.CLUSTER)
+    }
+
+    @Test
+    void testUpdateStateUseGtid() throws Exception {
+        testRunner.setProperty(CaptureChangeMySQL.DRIVER_LOCATION, 'file:///path/to/mysql-connector-java-5.1.38-bin.jar')
+        testRunner.setProperty(CaptureChangeMySQL.HOSTS, 'localhost:3306')
+        testRunner.setProperty(CaptureChangeMySQL.USERNAME, 'root')
+        testRunner.setProperty(CaptureChangeMySQL.PASSWORD, 'password')
+        testRunner.setProperty(CaptureChangeMySQL.CONNECT_TIMEOUT, '2 seconds')
+        testRunner.setProperty(CaptureChangeMySQL.USE_BINLOG_GTID, 'true')
+
+        testRunner.run(1, false, true)
+
+        // GTID
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.GTID, nextPosition: 2] as EventHeaderV4,
+                [gtid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1'] as GtidEventData
+        ))
+
+        // BEGIN
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.QUERY, nextPosition: 4] as EventHeaderV4,
+                [database: 'myDB', sql: 'BEGIN'] as QueryEventData
+        ))
+
+        // COMMIT
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.XID, nextPosition: 6] as EventHeaderV4,
+                {} as EventData
+        ))
+
+        testRunner.run(1, false, false)
+
+        // Ensure state not set, as the processor hasn't been stopped and no State Update Interval has been set
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, null, Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, null, Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, null, Scope.CLUSTER)
+
+        // Stop the processor and verify the state is set
+        testRunner.run(1, true, false)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, '', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, '-1000', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1-1', Scope.CLUSTER)
+
+        testRunner.stateManager.clear(Scope.CLUSTER)
+
+        // Send some events, wait for the State Update Interval, and verify the state was set
+        testRunner.setProperty(CaptureChangeMySQL.STATE_UPDATE_INTERVAL, '1 second')
+        testRunner.run(1, false, true)
+
+        // GTID
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.GTID, nextPosition: 8] as EventHeaderV4,
+                [gtid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:2'] as GtidEventData
+        ))
+
+        // BEGIN
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.QUERY, nextPosition: 10] as EventHeaderV4,
+                [database: 'myDB', sql: 'BEGIN'] as QueryEventData
+        ))
+
+        // COMMIT
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.XID, nextPosition: 12] as EventHeaderV4,
+                {} as EventData
+        ))
+
+        sleep(1000)
+
+        testRunner.run(1, false, false)
+
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, '', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, '-1000', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:2-2', Scope.CLUSTER)
+
+        // GTID
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.GTID, nextPosition: 14] as EventHeaderV4,
+                [gtid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:3'] as GtidEventData
+        ))
+
+        // BEGIN
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.QUERY, nextPosition: 16] as EventHeaderV4,
+                [database: 'myDB', sql: 'BEGIN'] as QueryEventData
+        ))
+
+        // COMMIT
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.XID, nextPosition: 18] as EventHeaderV4,
+                {} as EventData
+        ))
+
+        testRunner.run(1, true, false)
 
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_FILENAME_KEY, '', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_POSITION_KEY, '-1000', Scope.CLUSTER)
+        testRunner.stateManager.assertStateEquals(BinlogEventInfo.BINLOG_GTIDSET_KEY, 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:2-3', Scope.CLUSTER)
     }
 
     @Test
@@ -936,6 +1039,96 @@ class CaptureChangeMySQLTest {
         assertEquals(1, resultFiles.size())
     }
 
+    @Test
+    void testInitialGtidIgnoredWhenStatePresent() throws Exception {
+        testRunner.setProperty(CaptureChangeMySQL.DRIVER_LOCATION, 'file:///path/to/mysql-connector-java-5.1.38-bin.jar')
+        testRunner.setProperty(CaptureChangeMySQL.HOSTS, 'localhost:3306')
+        testRunner.setProperty(CaptureChangeMySQL.USERNAME, 'root')
+        testRunner.setProperty(CaptureChangeMySQL.CONNECT_TIMEOUT, '2 seconds')
+        testRunner.setProperty(CaptureChangeMySQL.USE_BINLOG_GTID, 'true')
+        testRunner.setProperty(CaptureChangeMySQL.INIT_BINLOG_GTID, 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1')
+        testRunner.setProperty(CaptureChangeMySQL.INIT_SEQUENCE_ID, '10')
+        testRunner.setProperty(CaptureChangeMySQL.RETRIEVE_ALL_RECORDS, 'false')
+        testRunner.setProperty(CaptureChangeMySQL.INCLUDE_BEGIN_COMMIT, 'true')
+        testRunner.getStateManager().setState([
+                ("${BinlogEventInfo.BINLOG_GTIDSET_KEY}".toString()): 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:2',
+                ("${EventWriter.SEQUENCE_ID_KEY}".toString()): '1'
+        ], Scope.CLUSTER)
+
+        testRunner.run(1, false, true)
+
+        // GTID
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.GTID, nextPosition: 2] as EventHeaderV4,
+                [gtid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:3'] as GtidEventData
+        ))
+
+        // BEGIN
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.QUERY, nextPosition: 4] as EventHeaderV4,
+                [database: 'myDB', sql: 'BEGIN'] as QueryEventData
+        ))
+
+        // COMMIT
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.XID, nextPosition: 12] as EventHeaderV4,
+                {} as EventData
+        ))
+
+        testRunner.run(1, true, false)
+
+        def resultFiles = testRunner.getFlowFilesForRelationship(CaptureChangeMySQL.REL_SUCCESS)
+
+        assertEquals(2, resultFiles.size())
+        assertEquals(
+                'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:2-3',
+                resultFiles.last().getAttribute(BinlogEventInfo.BINLOG_GTIDSET_KEY)
+        )
+    }
+
+    @Test
+    void testInitialGtidNoStatePresent() throws Exception {
+        testRunner.setProperty(CaptureChangeMySQL.DRIVER_LOCATION, 'file:///path/to/mysql-connector-java-5.1.38-bin.jar')
+        testRunner.setProperty(CaptureChangeMySQL.HOSTS, 'localhost:3306')
+        testRunner.setProperty(CaptureChangeMySQL.USERNAME, 'root')
+        testRunner.setProperty(CaptureChangeMySQL.PASSWORD, 'password')
+        testRunner.setProperty(CaptureChangeMySQL.CONNECT_TIMEOUT, '2 seconds')
+        testRunner.setProperty(CaptureChangeMySQL.USE_BINLOG_GTID, 'true')
+        testRunner.setProperty(CaptureChangeMySQL.INIT_BINLOG_GTID, 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1')
+        testRunner.setProperty(CaptureChangeMySQL.RETRIEVE_ALL_RECORDS, 'false')
+        testRunner.setProperty(CaptureChangeMySQL.INCLUDE_BEGIN_COMMIT, 'true')
+
+        testRunner.run(1, false, true)
+
+        // GTID
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.GTID, nextPosition: 2] as EventHeaderV4,
+                [gtid: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:3'] as GtidEventData
+        ))
+
+        // BEGIN
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.QUERY, nextPosition: 4] as EventHeaderV4,
+                [database: 'myDB', sql: 'BEGIN'] as QueryEventData
+        ))
+
+        // COMMIT
+        client.sendEvent(new Event(
+                [timestamp: new Date().time, eventType: EventType.XID, nextPosition: 12] as EventHeaderV4,
+                {} as EventData
+        ))
+
+        testRunner.run(1, true, false)
+
+        def resultFiles = testRunner.getFlowFilesForRelationship(CaptureChangeMySQL.REL_SUCCESS)
+
+        assertEquals(2, resultFiles.size())
+        assertEquals(
+                'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx:1-1:3-3',
+                resultFiles.last().getAttribute(BinlogEventInfo.BINLOG_GTIDSET_KEY)
+        )
+    }
+
     /********************************
      * Mock and helper classes below
      ********************************/


[nifi] 04/11: NIFI-7807 Updating in-class documentation to be more clear & adding additionalDetails with examples

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 33d9b8fdcaa787334a8acbd79e961396c9bf2e74
Author: abrown <ab...@cloudera.com>
AuthorDate: Mon Sep 14 13:53:48 2020 +0100

    NIFI-7807 Updating in-class documentation to be more clear & adding additionalDetails with examples
    
    Signed-off-by: Pierre Villard <pi...@gmail.com>
    
    This closes #4526.
---
 .../org/apache/nifi/processors/kudu/PutKudu.java   |   2 +-
 .../additionalDetails.html                         | 185 +++++++++++++++++++++
 2 files changed, 186 insertions(+), 1 deletion(-)

diff --git a/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/java/org/apache/nifi/processors/kudu/PutKudu.java b/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/java/org/apache/nifi/processors/kudu/PutKudu.java
index 064e295..3e8e199 100644
--- a/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/java/org/apache/nifi/processors/kudu/PutKudu.java
+++ b/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/java/org/apache/nifi/processors/kudu/PutKudu.java
@@ -75,7 +75,7 @@ import java.util.stream.Stream;
 @InputRequirement(InputRequirement.Requirement.INPUT_REQUIRED)
 @Tags({"put", "database", "NoSQL", "kudu", "HDFS", "record"})
 @CapabilityDescription("Reads records from an incoming FlowFile using the provided Record Reader, and writes those records " +
-        "to the specified Kudu's table. The schema for the table must be provided in the processor properties or from your source." +
+        "to the specified Kudu's table. The schema for the Kudu table is inferred from the schema of the Record Reader." +
         " If any error occurs while reading records from the input, or writing records to Kudu, the FlowFile will be routed to failure")
 @WritesAttribute(attribute = "record.count", description = "Number of records written to Kudu")
 
diff --git a/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/resources/docs/org.apache.nifi.processors.kudu.PutKudu/additionalDetails.html b/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/resources/docs/org.apache.nifi.processors.kudu.PutKudu/additionalDetails.html
new file mode 100644
index 0000000..bb79092
--- /dev/null
+++ b/nifi-nar-bundles/nifi-kudu-bundle/nifi-kudu-processors/src/main/resources/docs/org.apache.nifi.processors.kudu.PutKudu/additionalDetails.html
@@ -0,0 +1,185 @@
+<!DOCTYPE html>
+<html lang="en">
+    <!--
+      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.
+    -->
+    <head>
+        <meta charset="utf-8" />
+        <title>PutKudu</title>
+
+        <link rel="stylesheet" href="../../../../../css/component-usage.css" type="text/css" />
+    </head>
+
+    <body>
+        <!-- Processor Documentation ================================================== -->
+        <h2>Description:</h2>
+        <p>
+            This processor writes Records to a Kudu table. 
+            
+            A Record Reader must be supplied to read the records from the FlowFile.
+            The schema supplied to the Record Reader is used to match fields in the Record to the columns of the Kudu table.
+            See the <a href='#tableSchema'>Table Schema</a> section for more.
+        </p>
+
+        <h3>Table Name</h3>
+        <p>When Hive MetaStore integration is enabled for Impala/Kudu, do not use the "impala::" syntax for the table name. Simply use the Hive "dbname.tablename" syntax.</p>
+
+        <p>
+            For example, without HMS integration, you might use
+        </p>
+        <code>
+            <pre>
+                Table Name: impala::default.testtable
+            </pre>
+        </code>
+        <p>
+            With HMS integration, you would simply use
+        </p>
+        <code>
+            <pre>
+                Table Name: default.testtable
+            </pre>
+        </code>
+
+
+        <h3 name='tableSchema'>Table Schema</h3>
+
+        <p>
+            When writing to Kudu, NiFi must map the fields from the Record to the columns of the Kudu table. It does this by acquiring the schema of the table from Kudu and the schema
+            provided by the Record Reader. It can now compare the Record field names against the Kudu table column names. Additionally, it also compares the field and colunm types, to
+            apply the appropriate type conversions.
+        </p>
+
+        <p>
+            For example, assuming you have the following data:
+        </p>
+
+        <code>
+            <pre>
+                {
+                    "forename":"Jessica",
+                    "surname":"Smith",
+                    "employee_id":123456789
+                }
+            </pre>
+        </code>
+
+        <p>
+            With the following schema in the Record Reader:
+        </p>
+
+        <code>
+            <pre>
+                {
+                    "type": "record",
+                    "namespace": "nifi",
+                    "name": "employee",
+                    "fields": [
+                        { "name": "forename", "type": "string" },
+                        { "name": "surname", "type": "string" },
+                        { "name": "employee_id", "type": "long" }
+                    ]
+                }
+            </pre>
+        </code>
+
+        <p>
+           With a Kudu table created via Impala using the following create table:
+        </p>
+
+        <code>
+            <pre>
+                CREATE TABLE employees
+                (
+                forename STRING,
+                surname STRING,
+                employee_id BIGINT,
+                PRIMARY KEY(employee_id)
+                )
+                PARTITION BY HASH PARTITIONS 16
+                STORED AS KUDU; 
+            </pre>
+        </code>
+
+           <p>NiFi will acquire the table schema from Kudu, so it knows the column names and types. (e.g. forename STRING, surname STRING, employee_id BIGINT)
+           Then, it matches the Record field names against the Kudu column names (e.g. record forename -> column forename, etc.)
+           Next, it matches the Record data types to the column data types. See the <a href='#dataTypes'>Data Types</a> section for more.</p>
+
+           <p>Where there is deviation in Record schema and Table schema, there is two existing options.</p>
+
+           <p>Firstly, the <b>Lowercase Field Names</b> option allows NiFi to handle differences in casing.
+           For example, if your Kudu columns were FORENAME, SURNAME and EMPLOYEE_ID these would not match the Record Schema above, as they are case senstive.
+           This option would simply convert the names to lowercase for the purpose of comparison. It <b>does not</b> change the Kudu table schema.</p>
+
+           <p>Secondly, the <b>Handle Schema Drift</b> options allows for un-matched fields to be added to the table schema. This <b>does</b> modify the Kudu table schema.
+           For example, if we add a "dateOfBirth" field to the above data & record schema examples, these would not map to a column in the Kudu table.
+           With this option enabled, NiFi would modify the Kudu table to add a new column called "dateOfBirth" and then insert the Record.</p>
+
+        
+        
+        <h3 name='dataTypes'>Data Types</h3>
+        <p>NiFi data types are mapped to the following Kudu types:</p>
+        <table>
+            <tr>
+                <th>NiFi Type</th>
+                <th>Kudu Type</th>
+            </tr>
+            <tr>
+                <td>BOOLEAN</td>
+                <td>BOOL</td>
+            </tr>
+            <tr>
+                <td>BYTE</td>
+                <td>INT8</td>
+            </tr>
+            <tr>
+                <td>SHORT</td>
+                <td>INT16</td>
+            </tr>
+            <tr>
+                <td>INT</td>
+                <td>INT32</td>
+            </tr>
+            <tr>
+                <td>LONG</td>
+                <td>INT64</td>
+            </tr>
+            <tr>
+                <td>FLOAT</td>
+                <td>FLOAT</td>
+            </tr>
+            <tr>
+                <td>DOUBLE</td>
+                <td>DOUBLE</td>
+            </tr>
+            <tr>
+                <td>DECIMAL</td>
+                <td>DECIMAL</td>
+            </tr>
+            <tr>
+                <td>TIMESTAMP</td>
+                <td>UNIXTIME_MICROS</td>
+            </tr>
+            <tr>
+                <td>STRING</td>
+                <td>STRING</td>
+            </tr>
+            <tr>
+                <td>CHAR</td>
+                <td>STRING</td>
+            </tr>
+        </table>
+
+    </body>
+</html>


[nifi] 03/11: NIFI-7742: remove defined and null check

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit b9b31b299b6d5c21681d1b1d827f5fba89bb4a40
Author: Shane Ardell <sh...@gmail.com>
AuthorDate: Fri Sep 11 17:21:20 2020 -0400

    NIFI-7742: remove defined and null check
    
    This closes #4524
    
    Signed-off-by: Scott Aslan <sc...@gmail.com>
---
 .../canvas/controllers/nf-ng-canvas-flow-status-controller.js  | 10 ++++------
 1 file changed, 4 insertions(+), 6 deletions(-)

diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
index 5595803..4de9712 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
@@ -319,13 +319,11 @@
                                     break;
                                 case 'controller service':
                                     var group = item.parentGroup;
-                                    if (nfCommon.isDefinedAndNotNull(group.id)) {
-                                        nfProcessGroup.enterGroup(group.id).done(function () {
-                                            nfProcessGroupConfiguration.showConfiguration(group.id).done(function () {
-                                                nfProcessGroupConfiguration.selectControllerService(item.id);
-                                            });
+                                    nfProcessGroup.enterGroup(group.id).done(function () {
+                                        nfProcessGroupConfiguration.showConfiguration(group.id).done(function () {
+                                            nfProcessGroupConfiguration.selectControllerService(item.id);
                                         });
-                                    }
+                                    });
                                     break;
                                 default:
                                     var group = item.parentGroup;


[nifi] 11/11: NIFI-7787 fixing version references

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit c18a81dad91fd6b07c084c21f03d57007e2c5731
Author: Joe Witt <jo...@apache.org>
AuthorDate: Thu Sep 17 15:23:58 2020 -0700

    NIFI-7787 fixing version references
---
 nifi-commons/nifi-security-utils-api/pom.xml                      | 2 +-
 nifi-commons/nifi-security-utils/pom.xml                          | 2 +-
 .../nifi-extension-utils/nifi-kerberos-test-utils/pom.xml         | 8 ++++----
 nifi-nar-bundles/nifi-framework-bundle/pom.xml                    | 2 +-
 nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml   | 2 +-
 .../nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml            | 2 +-
 nifi-nar-bundles/pom.xml                                          | 2 +-
 7 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/nifi-commons/nifi-security-utils-api/pom.xml b/nifi-commons/nifi-security-utils-api/pom.xml
index 02dbe52..c953a2c 100644
--- a/nifi-commons/nifi-security-utils-api/pom.xml
+++ b/nifi-commons/nifi-security-utils-api/pom.xml
@@ -17,7 +17,7 @@
     <parent>
         <groupId>org.apache.nifi</groupId>
         <artifactId>nifi-commons</artifactId>
-        <version>1.13.0-SNAPSHOT</version>
+        <version>1.12.1-SNAPSHOT</version>
     </parent>
     <artifactId>nifi-security-utils-api</artifactId>
     <description>
diff --git a/nifi-commons/nifi-security-utils/pom.xml b/nifi-commons/nifi-security-utils/pom.xml
index 2b7bd58..b4289c2 100644
--- a/nifi-commons/nifi-security-utils/pom.xml
+++ b/nifi-commons/nifi-security-utils/pom.xml
@@ -40,7 +40,7 @@
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-security-utils-api</artifactId>
-            <version>1.13.0-SNAPSHOT</version>
+            <version>1.12.1-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>ch.qos.logback</groupId>
diff --git a/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml b/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml
index a223d03..3801ca9 100644
--- a/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml
+++ b/nifi-nar-bundles/nifi-extension-utils/nifi-kerberos-test-utils/pom.xml
@@ -18,7 +18,7 @@
     <parent>
         <groupId>org.apache.nifi</groupId>
         <artifactId>nifi-extension-utils</artifactId>
-        <version>1.13.0-SNAPSHOT</version>
+        <version>1.12.1-SNAPSHOT</version>
     </parent>
 
     <artifactId>nifi-kerberos-test-utils</artifactId>
@@ -27,17 +27,17 @@
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-api</artifactId>
-            <version>1.13.0-SNAPSHOT</version>
+            <version>1.12.1-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-utils</artifactId>
-            <version>1.13.0-SNAPSHOT</version>
+            <version>1.12.1-SNAPSHOT</version>
         </dependency>
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-kerberos-credentials-service-api</artifactId>
-            <version>1.13.0-SNAPSHOT</version>
+            <version>1.12.1-SNAPSHOT</version>
         </dependency>
     </dependencies>
 
diff --git a/nifi-nar-bundles/nifi-framework-bundle/pom.xml b/nifi-nar-bundles/nifi-framework-bundle/pom.xml
index db1e89e..0905d41 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/pom.xml
+++ b/nifi-nar-bundles/nifi-framework-bundle/pom.xml
@@ -56,7 +56,7 @@
             <dependency>
                 <groupId>org.apache.nifi</groupId>
                 <artifactId>nifi-security-utils-api</artifactId>
-                <version>1.13.0-SNAPSHOT</version>
+                <version>1.12.1-SNAPSHOT</version>
             </dependency>
             <dependency>
                 <groupId>org.apache.nifi</groupId>
diff --git a/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml b/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml
index 38a3234..ea6cb8f 100644
--- a/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml
+++ b/nifi-nar-bundles/nifi-hive-bundle/nifi-hive3-processors/pom.xml
@@ -150,7 +150,7 @@
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-kerberos-test-utils</artifactId>
-            <version>1.13.0-SNAPSHOT</version>
+            <version>1.12.1-SNAPSHOT</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
index 83ddffc..5de1149 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-dbcp-service-bundle/nifi-dbcp-service/pom.xml
@@ -117,7 +117,7 @@
         <dependency>
             <groupId>org.apache.nifi</groupId>
             <artifactId>nifi-kerberos-test-utils</artifactId>
-            <version>1.13.0-SNAPSHOT</version>
+            <version>1.12.1-SNAPSHOT</version>
             <scope>test</scope>
         </dependency>
         <dependency>
diff --git a/nifi-nar-bundles/pom.xml b/nifi-nar-bundles/pom.xml
index 84b9ccb..5582713 100755
--- a/nifi-nar-bundles/pom.xml
+++ b/nifi-nar-bundles/pom.xml
@@ -201,7 +201,7 @@
             <dependency>
                 <groupId>org.apache.nifi</groupId>
                 <artifactId>nifi-security-utils-api</artifactId>
-                <version>1.13.0-SNAPSHOT</version>
+                <version>1.12.1-SNAPSHOT</version>
                 <scope>provided</scope>
             </dependency>
             <dependency>


[nifi] 09/11: NIFI-7580-Add documentation around autoloading NARs

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 1adb16efb8e976c47e310bc10ac19e5d439b891d
Author: abrown <ab...@cloudera.com>
AuthorDate: Tue Sep 15 11:55:30 2020 +0100

    NIFI-7580-Add documentation around autoloading NARs
    
    Signed-off-by: Pierre Villard <pi...@gmail.com>
    
    This closes #4529.
---
 .../src/main/asciidoc/administration-guide.adoc    | 62 ++++++++++++++++++++++
 1 file changed, 62 insertions(+)

diff --git a/nifi-docs/src/main/asciidoc/administration-guide.adoc b/nifi-docs/src/main/asciidoc/administration-guide.adoc
index ea2919f..337c60b 100644
--- a/nifi-docs/src/main/asciidoc/administration-guide.adoc
+++ b/nifi-docs/src/main/asciidoc/administration-guide.adoc
@@ -3676,3 +3676,65 @@ In your new NiFi installation:
 3. After confirming your new NiFi instances are stable and working as expected, the old installation can be removed.
 
 NOTE:  If the original NiFi was setup to run as a service, update any symlinks or service scripts to point to the new NiFi version executables.
+
+
+== Processor Locations
+
+[[processor-location-options]]
+=== Available Configuration Options
+
+NiFi provides 3 configuration options for processor locations. Namely:
+
+   nifi.nar.library.directory
+   nifi.nar.library.directory.<custom>
+   nifi.nar.library.autoload.directory
+
+NOTE: Paths set using these options are relative to the NiFi Home Directory. For example, if the NiFi Home Directory is `/var/lib/nifi`, and the Library Directory is `./lib`, then the final path is `/var/lib/nifi/lib`.
+
+The `nifi.nar.library.directory` is used for the default location for provided NiFi processors. It is not recommended to use this for custom processors as these could be lost during a NiFi upgrade. For example:
+
+   nifi.nar.library.directory=./lib
+
+The `nifi.nar.library.directory.<custom>` allows the admin to provide multiple arbritary paths for NiFi to locate custom processors. A unique property identifier must append the property for each unique path. For example:
+
+   nifi.nar.library.directory.myCustomLibs=./my-custom-nars/lib
+   nifi.nar.library.directory.otherCustomLibs=./other-custom-nars/lib
+
+The `nifi.nar.library.autoload.directory` is used by the autoload feature, where NiFi can automatically load new processors added to the configured path without requiring a restart. For example:
+
+   nifi.nar.library.autoload.directory=./autoload/lib
+
+=== Installing Custom Processors
+
+This section describes the original process for installing custom processors that requires a restart to NiFi. To use the Autoloading feature, see the below <<autoloading-processors>> section.
+
+Firstly, we will configure a directory for the custom processors. See <<processor-location-options>> for more about these configuration options.
+
+   nifi.nar.library.directory.myCustomLibs=./my-custom-nars/lib
+ 
+Ensure that this directory exists and has appropriate permissions for the nifi user and group.
+
+Now, we must place our custom processor nar in the configured directory. The configured directory is relative to the NiFi Home directory; for example, let us say that our NiFi Home Dir is `/var/lib/nifi`, we would place our custom processor nar in `/var/lib/nifi/my-custom-nars/lib`.
+
+Ensure that the file has appropriate permissions for the nifi user and group.
+
+Restart NiFi and the custom processor should now be available when adding a new Processor to your flow.
+
+[[autoloading-processors]]
+=== Autoloading Custom Processors
+
+This section describes the process to use the Autoloading feature for custom processors.
+
+To use the autoloading feature, the `nifi.nar.library.autoload.directory` property must be configured to point at the desired directory. By default, this points at `./extensions`. 
+
+For example:
+
+   nifi.nar.library.autoload.directory=./extensions
+
+Ensure that this directory exists and has appropriate permissions for the nifi user and group.
+
+Now, we must place our custom processor nar in the configured directory. The configured directory is relative to the NiFi Home directory; for example, let us say that our NiFi Home Dir is `/var/lib/nifi`, we would place our custom processor nar in `/var/lib/nifi/extensions`.
+
+Ensure that the file has appropriate permissions for the nifi user and group.
+
+Refresh the browser page and the custom processor should now be available when adding a new Processor to your flow.


[nifi] 01/11: NIFI-7742: add case for controller service selections

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit c0e7f773f0fdd3af6f6bdf38aada3f4162bdae5f
Author: Shane Ardell <sh...@gmail.com>
AuthorDate: Fri Sep 11 15:49:51 2020 -0400

    NIFI-7742: add case for controller service selections
---
 .../nf-ng-canvas-flow-status-controller.js         | 39 ++++++++++++++++++----
 1 file changed, 33 insertions(+), 6 deletions(-)

diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
index 8a338fa..2e2ebc5 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
@@ -27,9 +27,11 @@
                 'nf.ClusterSummary',
                 'nf.ErrorHandler',
                 'nf.Settings',
-                'nf.ParameterContexts'],
-            function ($, nfCommon, nfDialog, nfCanvasUtils, nfContextMenu, nfClusterSummary, nfErrorHandler, nfSettings, nfParameterContexts) {
-                return (nf.ng.Canvas.FlowStatusCtrl = factory($, nfCommon, nfDialog, nfCanvasUtils, nfContextMenu, nfClusterSummary, nfErrorHandler, nfSettings, nfParameterContexts));
+                'nf.ParameterContexts',
+                'nf.ProcessGroup',
+                'nf.ProcessGroupConfiguration'],
+            function ($, nfCommon, nfDialog, nfCanvasUtils, nfContextMenu, nfClusterSummary, nfErrorHandler, nfSettings, nfParameterContexts, nfProcessGroup, nfProcessGroupConfiguration) {
+                return (nf.ng.Canvas.FlowStatusCtrl = factory($, nfCommon, nfDialog, nfCanvasUtils, nfContextMenu, nfClusterSummary, nfErrorHandler, nfSettings, nfParameterContexts, nfProcessGroup, nfProcessGroupConfiguration));
             });
     } else if (typeof exports === 'object' && typeof module === 'object') {
         module.exports = (nf.ng.Canvas.FlowStatusCtrl =
@@ -41,7 +43,9 @@
                 require('nf.ClusterSummary'),
                 require('nf.ErrorHandler'),
                 require('nf.Settings'),
-                require('nf.ParameterContexts')));
+                require('nf.ParameterContexts'),
+                require('nf.ProcessGroup'),
+                require('nf.ProcessGroupConfiguration')));
     } else {
         nf.ng.Canvas.FlowStatusCtrl = factory(root.$,
             root.nf.Common,
@@ -51,9 +55,11 @@
             root.nf.ClusterSummary,
             root.nf.ErrorHandler,
             root.nf.Settings,
-            root.nf.ParameterContexts);
+            root.nf.ParameterContexts,
+            root.nf.ProcessGroup,
+            root.nf.ProcessGroupConfiguration);
     }
-}(this, function ($, nfCommon, nfDialog, nfCanvasUtils, nfContextMenu, nfClusterSummary, nfErrorHandler, nfSettings, nfParameterContexts) {
+}(this, function ($, nfCommon, nfDialog, nfCanvasUtils, nfContextMenu, nfClusterSummary, nfErrorHandler, nfSettings, nfParameterContexts, nfProcessGroup, nfProcessGroupConfiguration) {
     'use strict';
 
     return function (serviceProvider) {
@@ -311,6 +317,27 @@
                                     var paramContext = item.parentGroup;
                                     nfParameterContexts.showParameterContext(paramContext.id, null, item.name);
                                     break;
+                                case 'controller service':
+                                    var group = item.parentGroup;
+                                    if (nfCommon.isDefinedAndNotNull(group.id)) {
+                                        nfProcessGroup.enterGroup(group.id).done(function () {
+                                            if ($('#process-group-configuration').is(':visible')) {
+                                                nfProcessGroupConfiguration.loadConfiguration(group.id).done(function () {
+                                                    nfProcessGroupConfiguration.selectControllerService(item.id);
+                                                });
+                                            } else {
+                                                nfProcessGroupConfiguration.showConfiguration(group.id).done(function () {
+                                                    nfSettings.selectControllerService(item.id);
+                                                });
+                                            }
+                                        });
+                                    } else {
+                                        // reload the settings and show
+                                        nfSettings.showSettings().done(function () {
+                                            nfSettings.selectControllerService(item.id);
+                                        });
+                                    }
+                                    break;
                                 default:
                                     var group = item.parentGroup;
 


[nifi] 02/11: NIFI-7742: update case clause logic

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 57eb740384e7cb9f7268d5660587dc82316d5a52
Author: Shane Ardell <sh...@gmail.com>
AuthorDate: Fri Sep 11 16:43:24 2020 -0400

    NIFI-7742: update case clause logic
---
 .../controllers/nf-ng-canvas-flow-status-controller.js  | 17 +++--------------
 1 file changed, 3 insertions(+), 14 deletions(-)

diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
index 2e2ebc5..5595803 100644
--- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
+++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js
@@ -321,20 +321,9 @@
                                     var group = item.parentGroup;
                                     if (nfCommon.isDefinedAndNotNull(group.id)) {
                                         nfProcessGroup.enterGroup(group.id).done(function () {
-                                            if ($('#process-group-configuration').is(':visible')) {
-                                                nfProcessGroupConfiguration.loadConfiguration(group.id).done(function () {
-                                                    nfProcessGroupConfiguration.selectControllerService(item.id);
-                                                });
-                                            } else {
-                                                nfProcessGroupConfiguration.showConfiguration(group.id).done(function () {
-                                                    nfSettings.selectControllerService(item.id);
-                                                });
-                                            }
-                                        });
-                                    } else {
-                                        // reload the settings and show
-                                        nfSettings.showSettings().done(function () {
-                                            nfSettings.selectControllerService(item.id);
+                                            nfProcessGroupConfiguration.showConfiguration(group.id).done(function () {
+                                                nfProcessGroupConfiguration.selectControllerService(item.id);
+                                            });
                                         });
                                     }
                                     break;


[nifi] 06/11: NIFI-7800: Fix line endings for changed files

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 05d445f34d316bb0287f73ff71854482071d596c
Author: Matthew Burgess <ma...@apache.org>
AuthorDate: Mon Sep 14 11:46:14 2020 -0400

    NIFI-7800: Fix line endings for changed files
    
    Signed-off-by: Pierre Villard <pi...@gmail.com>
    
    This closes #4527.
---
 .../java/org/apache/nifi/xml/WriteXMLResult.java   | 1304 ++++-----
 .../org/apache/nifi/xml/TestWriteXMLResult.java    | 2860 ++++++++++----------
 2 files changed, 2082 insertions(+), 2082 deletions(-)

diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/xml/WriteXMLResult.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/xml/WriteXMLResult.java
index 6f5abc8..dc40e75 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/xml/WriteXMLResult.java
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/main/java/org/apache/nifi/xml/WriteXMLResult.java
@@ -1,652 +1,652 @@
-/*
- * 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.xml;
-
-import javanet.staxutils.IndentingXMLStreamWriter;
-import org.apache.nifi.record.NullSuppression;
-import org.apache.nifi.schema.access.SchemaAccessWriter;
-import org.apache.nifi.serialization.AbstractRecordSetWriter;
-import org.apache.nifi.serialization.RecordSetWriter;
-import org.apache.nifi.serialization.WriteResult;
-import org.apache.nifi.serialization.record.DataType;
-import org.apache.nifi.serialization.record.RawRecordWriter;
-import org.apache.nifi.serialization.record.Record;
-import org.apache.nifi.serialization.record.RecordField;
-import org.apache.nifi.serialization.record.RecordFieldType;
-import org.apache.nifi.serialization.record.RecordSchema;
-import org.apache.nifi.serialization.record.type.ArrayDataType;
-import org.apache.nifi.serialization.record.type.ChoiceDataType;
-import org.apache.nifi.serialization.record.type.MapDataType;
-import org.apache.nifi.serialization.record.type.RecordDataType;
-import org.apache.nifi.serialization.record.util.DataTypeUtils;
-
-import javax.xml.stream.XMLOutputFactory;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.stream.XMLStreamWriter;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.text.DateFormat;
-import java.util.ArrayDeque;
-import java.util.Deque;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Supplier;
-import java.util.regex.Pattern;
-
-import static org.apache.nifi.xml.XMLRecordSetWriter.RECORD_TAG_NAME;
-import static org.apache.nifi.xml.XMLRecordSetWriter.ROOT_TAG_NAME;
-
-
-public class WriteXMLResult extends AbstractRecordSetWriter implements RecordSetWriter, RawRecordWriter {
-    private static final Pattern TAG_NAME_CHARS_TO_STRIP = Pattern.compile("[/<>!&'\"]");
-
-    private final RecordSchema recordSchema;
-    private final SchemaAccessWriter schemaAccess;
-    private final XMLStreamWriter writer;
-    private final NullSuppression nullSuppression;
-    private final boolean omitDeclaration;
-    private final ArrayWrapping arrayWrapping;
-    private final String arrayTagName;
-    private final String recordTagName;
-    private final String rootTagName;
-    private final boolean allowWritingMultipleRecords;
-    private boolean hasWrittenRecord;
-
-    private final Supplier<DateFormat> LAZY_DATE_FORMAT;
-    private final Supplier<DateFormat> LAZY_TIME_FORMAT;
-    private final Supplier<DateFormat> LAZY_TIMESTAMP_FORMAT;
-
-    public WriteXMLResult(final RecordSchema recordSchema, final SchemaAccessWriter schemaAccess, final OutputStream out, final boolean prettyPrint, final boolean omitDeclaration,
-                          final NullSuppression nullSuppression, final ArrayWrapping arrayWrapping, final String arrayTagName, final String rootTagName, final String recordTagName,
-                          final String charSet, final String dateFormat, final String timeFormat, final String timestampFormat) throws IOException {
-
-        super(out);
-
-        this.recordSchema = recordSchema;
-        this.schemaAccess = schemaAccess;
-        this.nullSuppression = nullSuppression;
-
-        this.omitDeclaration = omitDeclaration;
-
-        this.arrayWrapping = arrayWrapping;
-        this.arrayTagName = arrayTagName;
-
-        this.rootTagName = rootTagName;
-
-        if (recordTagName != null) {
-            this.recordTagName = recordTagName;
-        } else {
-            Optional<String> recordTagNameOptional = recordSchema.getSchemaName().isPresent()? recordSchema.getSchemaName() : recordSchema.getIdentifier().getName();
-            if (recordTagNameOptional.isPresent()) {
-                this.recordTagName = recordTagNameOptional.get();
-            } else {
-                final String message = "The property '" + RECORD_TAG_NAME.getDisplayName() +
-                    "' has not been set and the writer does not find a record name in the schema.";
-                throw new IOException(message);
-            }
-        }
-
-        this.allowWritingMultipleRecords = !(this.rootTagName == null);
-        hasWrittenRecord = false;
-
-        final DateFormat df = dateFormat == null ? null : DataTypeUtils.getDateFormat(dateFormat);
-        final DateFormat tf = timeFormat == null ? null : DataTypeUtils.getDateFormat(timeFormat);
-        final DateFormat tsf = timestampFormat == null ? null : DataTypeUtils.getDateFormat(timestampFormat);
-
-        LAZY_DATE_FORMAT = () -> df;
-        LAZY_TIME_FORMAT = () -> tf;
-        LAZY_TIMESTAMP_FORMAT = () -> tsf;
-
-        try {
-            XMLOutputFactory factory = XMLOutputFactory.newInstance();
-
-            if (prettyPrint) {
-                writer = new IndentingXMLStreamWriter(factory.createXMLStreamWriter(out, charSet));
-            } else {
-                writer = factory.createXMLStreamWriter(out, charSet);
-            }
-
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-    }
-
-    @Override
-    protected void onBeginRecordSet() throws IOException {
-
-        final OutputStream out = getOutputStream();
-        schemaAccess.writeHeader(recordSchema, out);
-
-        try {
-            if (!omitDeclaration) {
-                writer.writeStartDocument();
-            }
-
-            if (allowWritingMultipleRecords) {
-                writer.writeStartElement(rootTagName);
-            }
-
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-    }
-
-    @Override
-    protected Map<String, String> onFinishRecordSet() throws IOException {
-
-        try {
-            if (allowWritingMultipleRecords) {
-                writer.writeEndElement();
-            }
-
-            writer.writeEndDocument();
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-        return schemaAccess.getAttributes(recordSchema);
-    }
-
-    @Override
-    public void close() throws IOException {
-
-        try {
-            writer.close();
-
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-
-        super.close();
-    }
-
-    @Override
-    public void flush() throws IOException {
-
-        try {
-            writer.flush();
-
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-    }
-
-    private void checkWritingMultipleRecords() throws IOException {
-        if (!allowWritingMultipleRecords && hasWrittenRecord) {
-            final String message = "The writer attempts to write multiple record although property \'" + ROOT_TAG_NAME.getDisplayName() +
-                "\' has not been set. If the XMLRecordSetWriter is supposed to write multiple records into one FlowFile, this property is required to be configured.";
-            throw new IOException(message);
-        }
-    }
-
-    @Override
-    protected Map<String, String> writeRecord(Record record) throws IOException {
-
-        if (!isActiveRecordSet()) {
-            schemaAccess.writeHeader(recordSchema, getOutputStream());
-        }
-
-        checkWritingMultipleRecords();
-
-        Deque<String> tagsToOpen = new ArrayDeque<>();
-
-        try {
-            tagsToOpen.addLast(recordTagName);
-
-            boolean closingTagRequired = iterateThroughRecordUsingSchema(tagsToOpen, record, recordSchema);
-            if (closingTagRequired) {
-                writer.writeEndElement();
-                hasWrittenRecord = true;
-            }
-
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-        return schemaAccess.getAttributes(recordSchema);
-    }
-
-    private boolean iterateThroughRecordUsingSchema(Deque<String> tagsToOpen, Record record, RecordSchema schema) throws XMLStreamException {
-
-        boolean loopHasWritten = false;
-        for (RecordField field : schema.getFields()) {
-
-            String fieldName = field.getFieldName();
-            DataType dataType = field.getDataType();
-            Object value = record.getValue(field);
-
-            final DataType chosenDataType = dataType.getFieldType() == RecordFieldType.CHOICE ? DataTypeUtils.chooseDataType(value, (ChoiceDataType) dataType) : dataType;
-            final Object coercedValue = DataTypeUtils.convertType(value, chosenDataType, LAZY_DATE_FORMAT, LAZY_TIME_FORMAT, LAZY_TIMESTAMP_FORMAT, fieldName);
-
-            if (coercedValue != null) {
-                boolean hasWritten = writeFieldForType(tagsToOpen, coercedValue, chosenDataType, fieldName);
-                if (hasWritten) {
-                    loopHasWritten = true;
-                }
-
-            } else {
-                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING) && recordHasField(field, record)) {
-                    writeAllTags(tagsToOpen, fieldName);
-                    writer.writeEndElement();
-                    loopHasWritten = true;
-                }
-            }
-        }
-
-        return loopHasWritten;
-    }
-
-    private boolean writeFieldForType(Deque<String> tagsToOpen, Object coercedValue, DataType dataType, String fieldName) throws XMLStreamException {
-        switch (dataType.getFieldType()) {
-            case BOOLEAN:
-            case BYTE:
-            case CHAR:
-            case DECIMAL:
-            case DOUBLE:
-            case FLOAT:
-            case INT:
-            case LONG:
-            case SHORT:
-            case STRING: {
-                writeAllTags(tagsToOpen, fieldName);
-                writer.writeCharacters(coercedValue.toString());
-                writer.writeEndElement();
-                return true;
-            }
-            case DATE: {
-                writeAllTags(tagsToOpen, fieldName);
-                final String stringValue = DataTypeUtils.toString(coercedValue, LAZY_DATE_FORMAT);
-                writer.writeCharacters(stringValue);
-                writer.writeEndElement();
-                return true;
-            }
-            case TIME: {
-                writeAllTags(tagsToOpen, fieldName);
-                final String stringValue = DataTypeUtils.toString(coercedValue, LAZY_TIME_FORMAT);
-                writer.writeCharacters(stringValue);
-                writer.writeEndElement();
-                return true;
-            }
-            case TIMESTAMP: {
-                writeAllTags(tagsToOpen, fieldName);
-                final String stringValue = DataTypeUtils.toString(coercedValue, LAZY_TIMESTAMP_FORMAT);
-                writer.writeCharacters(stringValue);
-                writer.writeEndElement();
-                return true;
-            }
-            case RECORD: {
-                final Record record = (Record) coercedValue;
-                final RecordDataType recordDataType = (RecordDataType) dataType;
-                final RecordSchema childSchema = recordDataType.getChildSchema();
-                tagsToOpen.addLast(fieldName);
-
-                boolean hasWritten = iterateThroughRecordUsingSchema(tagsToOpen, record, childSchema);
-
-                if (hasWritten) {
-                    writer.writeEndElement();
-                    return true;
-                } else {
-
-                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                        writeAllTags(tagsToOpen);
-                        writer.writeEndElement();
-                        return true;
-                    } else {
-                        tagsToOpen.removeLast();
-                        return false;
-                    }
-                }
-            }
-            case ARRAY: {
-                final Object[] arrayValues;
-                if (coercedValue instanceof Object[]) {
-                    arrayValues = (Object[]) coercedValue;
-                } else {
-                    arrayValues = new Object[]{coercedValue.toString()};
-                }
-
-                final ArrayDataType arrayDataType = (ArrayDataType) dataType;
-                final DataType elementType = arrayDataType.getElementType();
-
-                final String elementName;
-                final String wrapperName;
-                if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_FOR_ELEMENTS)) {
-                    elementName = arrayTagName;
-                    wrapperName = fieldName;
-                } else if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_AS_WRAPPER)) {
-                    elementName = fieldName;
-                    wrapperName = arrayTagName;
-                } else {
-                    elementName = fieldName;
-                    wrapperName = null;
-                }
-
-                if (wrapperName!= null) {
-                    tagsToOpen.addLast(wrapperName);
-                }
-
-                boolean loopHasWritten = false;
-                for (Object element : arrayValues) {
-
-                    final DataType chosenDataType = elementType.getFieldType() == RecordFieldType.CHOICE ? DataTypeUtils.chooseDataType(element, (ChoiceDataType) elementType) : elementType;
-                    final Object coercedElement = DataTypeUtils.convertType(element, chosenDataType, LAZY_DATE_FORMAT, LAZY_TIME_FORMAT, LAZY_TIMESTAMP_FORMAT, elementName);
-
-                    if (coercedElement != null) {
-                        boolean hasWritten = writeFieldForType(tagsToOpen, coercedElement, elementType, elementName);
-
-                        if (hasWritten) {
-                            loopHasWritten = true;
-                        }
-
-                    } else {
-                        if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                            writeAllTags(tagsToOpen, fieldName);
-                            writer.writeEndElement();
-                            loopHasWritten = true;
-                        }
-                    }
-                }
-
-                if (wrapperName!= null) {
-                    if (loopHasWritten) {
-                        writer.writeEndElement();
-                        return true;
-                    } else {
-                        if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                            writeAllTags(tagsToOpen);
-                            writer.writeEndElement();
-                            return true;
-                        } else {
-                            tagsToOpen.removeLast();
-                            return false;
-                        }
-                    }
-                } else {
-                    return loopHasWritten;
-                }
-            }
-            case MAP: {
-                final MapDataType mapDataType = (MapDataType) dataType;
-                final DataType valueDataType = mapDataType.getValueType();
-                final Map<String,?> map = (Map<String,?>) coercedValue;
-
-                tagsToOpen.addLast(fieldName);
-                boolean loopHasWritten = false;
-
-                for (Map.Entry<String,?> entry : map.entrySet()) {
-
-                    final String key = entry.getKey();
-
-                    final DataType chosenDataType = valueDataType.getFieldType() == RecordFieldType.CHOICE ? DataTypeUtils.chooseDataType(entry.getValue(),
-                            (ChoiceDataType) valueDataType) : valueDataType;
-                    final Object coercedElement = DataTypeUtils.convertType(entry.getValue(), chosenDataType, LAZY_DATE_FORMAT, LAZY_TIME_FORMAT, LAZY_TIMESTAMP_FORMAT, key);
-
-                    if (coercedElement != null) {
-                        boolean hasWritten = writeFieldForType(tagsToOpen, entry.getValue(), valueDataType, key);
-
-                        if (hasWritten) {
-                            loopHasWritten = true;
-                        }
-                    } else {
-                        if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                            writeAllTags(tagsToOpen, key);
-                            writer.writeEndElement();
-                            loopHasWritten = true;
-                        }
-                    }
-                }
-
-                if (loopHasWritten) {
-                    writer.writeEndElement();
-                    return true;
-                } else {
-                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                        writeAllTags(tagsToOpen);
-                        writer.writeEndElement();
-                        return true;
-                    } else {
-                        tagsToOpen.removeLast();
-                        return false;
-                    }
-                }
-            }
-            case CHOICE:
-            default: {
-                return writeUnknownField(tagsToOpen, coercedValue, fieldName);
-            }
-        }
-    }
-
-    private void writeAllTags(Deque<String> tagsToOpen, String fieldName) throws XMLStreamException {
-        tagsToOpen.addLast(fieldName);
-        writeAllTags(tagsToOpen);
-    }
-
-    private String escapeTagName(final String tagName) {
-        return TAG_NAME_CHARS_TO_STRIP.matcher(tagName).replaceAll("");
-    }
-
-    private void writeAllTags(Deque<String> tagsToOpen) throws XMLStreamException {
-        for (String tagName : tagsToOpen) {
-            writer.writeStartElement(escapeTagName(tagName));
-        }
-        tagsToOpen.clear();
-    }
-
-    @Override
-    public WriteResult writeRawRecord(Record record) throws IOException {
-
-        if (!isActiveRecordSet()) {
-            schemaAccess.writeHeader(recordSchema, getOutputStream());
-        }
-
-        checkWritingMultipleRecords();
-
-        Deque<String> tagsToOpen = new ArrayDeque<>();
-
-        try {
-            tagsToOpen.addLast(recordTagName);
-
-            boolean closingTagRequired = iterateThroughRecordWithoutSchema(tagsToOpen, record);
-            if (closingTagRequired) {
-                writer.writeEndElement();
-                hasWrittenRecord = true;
-            }
-
-        } catch (XMLStreamException e) {
-            throw new IOException(e.getMessage());
-        }
-
-        final Map<String, String> attributes = schemaAccess.getAttributes(recordSchema);
-        return WriteResult.of(incrementRecordCount(), attributes);
-    }
-
-    private boolean iterateThroughRecordWithoutSchema(Deque<String> tagsToOpen, Record record) throws XMLStreamException {
-
-        boolean loopHasWritten = false;
-
-        for (String fieldName : record.getRawFieldNames()) {
-            Object value = record.getValue(fieldName);
-
-            if (value != null) {
-                boolean hasWritten = writeUnknownField(tagsToOpen, value, fieldName);
-
-                if (hasWritten) {
-                    loopHasWritten = true;
-                }
-            } else {
-                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                    writeAllTags(tagsToOpen, fieldName);
-                    writer.writeEndElement();
-                    loopHasWritten = true;
-                }
-            }
-        }
-
-        return loopHasWritten;
-    }
-
-    private boolean writeUnknownField(Deque<String> tagsToOpen, Object value, String fieldName) throws XMLStreamException {
-
-        if (value instanceof Record) {
-            Record valueAsRecord = (Record) value;
-            tagsToOpen.addLast(fieldName);
-
-            boolean hasWritten = iterateThroughRecordWithoutSchema(tagsToOpen, valueAsRecord);
-
-            if (hasWritten) {
-                writer.writeEndElement();
-                return true;
-            } else {
-                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                    writeAllTags(tagsToOpen);
-                    writer.writeEndElement();
-                    return true;
-                } else {
-                    tagsToOpen.removeLast();
-                    return false;
-                }
-            }
-        }
-
-        if (value instanceof Object[]) {
-            Object[] valueAsArray = (Object[]) value;
-
-            final String elementName;
-            final String wrapperName;
-            if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_FOR_ELEMENTS)) {
-                elementName = arrayTagName;
-                wrapperName = fieldName;
-            } else if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_AS_WRAPPER)) {
-                elementName = fieldName;
-                wrapperName = arrayTagName;
-            } else {
-                elementName = fieldName;
-                wrapperName = null;
-            }
-
-            if (wrapperName!= null) {
-                tagsToOpen.addLast(wrapperName);
-            }
-
-            boolean loopHasWritten = false;
-
-            for (Object element : valueAsArray) {
-                if (element != null) {
-                    boolean hasWritten = writeUnknownField(tagsToOpen, element, elementName);
-
-                    if (hasWritten) {
-                        loopHasWritten = true;
-                    }
-
-                } else {
-                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                        writeAllTags(tagsToOpen, fieldName);
-                        writer.writeEndElement();
-                        loopHasWritten = true;
-                    }
-                }
-            }
-
-            if (wrapperName!= null) {
-                if (loopHasWritten) {
-                    writer.writeEndElement();
-                    return true;
-                } else {
-                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                        writeAllTags(tagsToOpen);
-                        writer.writeEndElement();
-                        return true;
-                    } else {
-                        tagsToOpen.removeLast();
-                        return false;
-                    }
-                }
-            } else {
-                return loopHasWritten;
-            }
-        }
-
-        if (value instanceof Map) {
-            Map<String, ?> valueAsMap = (Map<String, ?>) value;
-
-            tagsToOpen.addLast(fieldName);
-            boolean loopHasWritten = false;
-
-            for (Map.Entry<String,?> entry : valueAsMap.entrySet()) {
-
-                final String key = entry.getKey();
-                final Object entryValue = entry.getValue();
-
-                if (entryValue != null) {
-                    boolean hasWritten = writeUnknownField(tagsToOpen, entry.getValue(), key);
-
-                    if (hasWritten) {
-                        loopHasWritten = true;
-                    }
-                } else {
-                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                        writeAllTags(tagsToOpen, key);
-                        writer.writeEndElement();
-                        loopHasWritten = true;
-                    }
-                }
-
-            }
-
-            if (loopHasWritten) {
-                writer.writeEndElement();
-                return true;
-            } else {
-                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
-                    writeAllTags(tagsToOpen);
-                    writer.writeEndElement();
-                    return true;
-                } else {
-                    tagsToOpen.removeLast();
-                    return false;
-                }
-            }
-        }
-
-        writeAllTags(tagsToOpen, fieldName);
-        writer.writeCharacters(value.toString());
-        writer.writeEndElement();
-        return true;
-    }
-
-
-    @Override
-    public String getMimeType() {
-        return "application/xml";
-    }
-
-    private boolean recordHasField(RecordField field, Record record) {
-        Set<String> recordFieldNames = record.getRawFieldNames();
-        if (recordFieldNames.contains(field.getFieldName())) {
-            return true;
-        }
-
-        for (String alias : field.getAliases()) {
-            if (recordFieldNames.contains(alias)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-}
+/*
+ * 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.xml;
+
+import javanet.staxutils.IndentingXMLStreamWriter;
+import org.apache.nifi.record.NullSuppression;
+import org.apache.nifi.schema.access.SchemaAccessWriter;
+import org.apache.nifi.serialization.AbstractRecordSetWriter;
+import org.apache.nifi.serialization.RecordSetWriter;
+import org.apache.nifi.serialization.WriteResult;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.RawRecordWriter;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.type.ArrayDataType;
+import org.apache.nifi.serialization.record.type.ChoiceDataType;
+import org.apache.nifi.serialization.record.type.MapDataType;
+import org.apache.nifi.serialization.record.type.RecordDataType;
+import org.apache.nifi.serialization.record.util.DataTypeUtils;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+import static org.apache.nifi.xml.XMLRecordSetWriter.RECORD_TAG_NAME;
+import static org.apache.nifi.xml.XMLRecordSetWriter.ROOT_TAG_NAME;
+
+
+public class WriteXMLResult extends AbstractRecordSetWriter implements RecordSetWriter, RawRecordWriter {
+    private static final Pattern TAG_NAME_CHARS_TO_STRIP = Pattern.compile("[/<>!&'\"]");
+
+    private final RecordSchema recordSchema;
+    private final SchemaAccessWriter schemaAccess;
+    private final XMLStreamWriter writer;
+    private final NullSuppression nullSuppression;
+    private final boolean omitDeclaration;
+    private final ArrayWrapping arrayWrapping;
+    private final String arrayTagName;
+    private final String recordTagName;
+    private final String rootTagName;
+    private final boolean allowWritingMultipleRecords;
+    private boolean hasWrittenRecord;
+
+    private final Supplier<DateFormat> LAZY_DATE_FORMAT;
+    private final Supplier<DateFormat> LAZY_TIME_FORMAT;
+    private final Supplier<DateFormat> LAZY_TIMESTAMP_FORMAT;
+
+    public WriteXMLResult(final RecordSchema recordSchema, final SchemaAccessWriter schemaAccess, final OutputStream out, final boolean prettyPrint, final boolean omitDeclaration,
+                          final NullSuppression nullSuppression, final ArrayWrapping arrayWrapping, final String arrayTagName, final String rootTagName, final String recordTagName,
+                          final String charSet, final String dateFormat, final String timeFormat, final String timestampFormat) throws IOException {
+
+        super(out);
+
+        this.recordSchema = recordSchema;
+        this.schemaAccess = schemaAccess;
+        this.nullSuppression = nullSuppression;
+
+        this.omitDeclaration = omitDeclaration;
+
+        this.arrayWrapping = arrayWrapping;
+        this.arrayTagName = arrayTagName;
+
+        this.rootTagName = rootTagName;
+
+        if (recordTagName != null) {
+            this.recordTagName = recordTagName;
+        } else {
+            Optional<String> recordTagNameOptional = recordSchema.getSchemaName().isPresent()? recordSchema.getSchemaName() : recordSchema.getIdentifier().getName();
+            if (recordTagNameOptional.isPresent()) {
+                this.recordTagName = recordTagNameOptional.get();
+            } else {
+                final String message = "The property '" + RECORD_TAG_NAME.getDisplayName() +
+                    "' has not been set and the writer does not find a record name in the schema.";
+                throw new IOException(message);
+            }
+        }
+
+        this.allowWritingMultipleRecords = !(this.rootTagName == null);
+        hasWrittenRecord = false;
+
+        final DateFormat df = dateFormat == null ? null : DataTypeUtils.getDateFormat(dateFormat);
+        final DateFormat tf = timeFormat == null ? null : DataTypeUtils.getDateFormat(timeFormat);
+        final DateFormat tsf = timestampFormat == null ? null : DataTypeUtils.getDateFormat(timestampFormat);
+
+        LAZY_DATE_FORMAT = () -> df;
+        LAZY_TIME_FORMAT = () -> tf;
+        LAZY_TIMESTAMP_FORMAT = () -> tsf;
+
+        try {
+            XMLOutputFactory factory = XMLOutputFactory.newInstance();
+
+            if (prettyPrint) {
+                writer = new IndentingXMLStreamWriter(factory.createXMLStreamWriter(out, charSet));
+            } else {
+                writer = factory.createXMLStreamWriter(out, charSet);
+            }
+
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    @Override
+    protected void onBeginRecordSet() throws IOException {
+
+        final OutputStream out = getOutputStream();
+        schemaAccess.writeHeader(recordSchema, out);
+
+        try {
+            if (!omitDeclaration) {
+                writer.writeStartDocument();
+            }
+
+            if (allowWritingMultipleRecords) {
+                writer.writeStartElement(rootTagName);
+            }
+
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    @Override
+    protected Map<String, String> onFinishRecordSet() throws IOException {
+
+        try {
+            if (allowWritingMultipleRecords) {
+                writer.writeEndElement();
+            }
+
+            writer.writeEndDocument();
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+        return schemaAccess.getAttributes(recordSchema);
+    }
+
+    @Override
+    public void close() throws IOException {
+
+        try {
+            writer.close();
+
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+
+        super.close();
+    }
+
+    @Override
+    public void flush() throws IOException {
+
+        try {
+            writer.flush();
+
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+    }
+
+    private void checkWritingMultipleRecords() throws IOException {
+        if (!allowWritingMultipleRecords && hasWrittenRecord) {
+            final String message = "The writer attempts to write multiple record although property \'" + ROOT_TAG_NAME.getDisplayName() +
+                "\' has not been set. If the XMLRecordSetWriter is supposed to write multiple records into one FlowFile, this property is required to be configured.";
+            throw new IOException(message);
+        }
+    }
+
+    @Override
+    protected Map<String, String> writeRecord(Record record) throws IOException {
+
+        if (!isActiveRecordSet()) {
+            schemaAccess.writeHeader(recordSchema, getOutputStream());
+        }
+
+        checkWritingMultipleRecords();
+
+        Deque<String> tagsToOpen = new ArrayDeque<>();
+
+        try {
+            tagsToOpen.addLast(recordTagName);
+
+            boolean closingTagRequired = iterateThroughRecordUsingSchema(tagsToOpen, record, recordSchema);
+            if (closingTagRequired) {
+                writer.writeEndElement();
+                hasWrittenRecord = true;
+            }
+
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+        return schemaAccess.getAttributes(recordSchema);
+    }
+
+    private boolean iterateThroughRecordUsingSchema(Deque<String> tagsToOpen, Record record, RecordSchema schema) throws XMLStreamException {
+
+        boolean loopHasWritten = false;
+        for (RecordField field : schema.getFields()) {
+
+            String fieldName = field.getFieldName();
+            DataType dataType = field.getDataType();
+            Object value = record.getValue(field);
+
+            final DataType chosenDataType = dataType.getFieldType() == RecordFieldType.CHOICE ? DataTypeUtils.chooseDataType(value, (ChoiceDataType) dataType) : dataType;
+            final Object coercedValue = DataTypeUtils.convertType(value, chosenDataType, LAZY_DATE_FORMAT, LAZY_TIME_FORMAT, LAZY_TIMESTAMP_FORMAT, fieldName);
+
+            if (coercedValue != null) {
+                boolean hasWritten = writeFieldForType(tagsToOpen, coercedValue, chosenDataType, fieldName);
+                if (hasWritten) {
+                    loopHasWritten = true;
+                }
+
+            } else {
+                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING) && recordHasField(field, record)) {
+                    writeAllTags(tagsToOpen, fieldName);
+                    writer.writeEndElement();
+                    loopHasWritten = true;
+                }
+            }
+        }
+
+        return loopHasWritten;
+    }
+
+    private boolean writeFieldForType(Deque<String> tagsToOpen, Object coercedValue, DataType dataType, String fieldName) throws XMLStreamException {
+        switch (dataType.getFieldType()) {
+            case BOOLEAN:
+            case BYTE:
+            case CHAR:
+            case DECIMAL:
+            case DOUBLE:
+            case FLOAT:
+            case INT:
+            case LONG:
+            case SHORT:
+            case STRING: {
+                writeAllTags(tagsToOpen, fieldName);
+                writer.writeCharacters(coercedValue.toString());
+                writer.writeEndElement();
+                return true;
+            }
+            case DATE: {
+                writeAllTags(tagsToOpen, fieldName);
+                final String stringValue = DataTypeUtils.toString(coercedValue, LAZY_DATE_FORMAT);
+                writer.writeCharacters(stringValue);
+                writer.writeEndElement();
+                return true;
+            }
+            case TIME: {
+                writeAllTags(tagsToOpen, fieldName);
+                final String stringValue = DataTypeUtils.toString(coercedValue, LAZY_TIME_FORMAT);
+                writer.writeCharacters(stringValue);
+                writer.writeEndElement();
+                return true;
+            }
+            case TIMESTAMP: {
+                writeAllTags(tagsToOpen, fieldName);
+                final String stringValue = DataTypeUtils.toString(coercedValue, LAZY_TIMESTAMP_FORMAT);
+                writer.writeCharacters(stringValue);
+                writer.writeEndElement();
+                return true;
+            }
+            case RECORD: {
+                final Record record = (Record) coercedValue;
+                final RecordDataType recordDataType = (RecordDataType) dataType;
+                final RecordSchema childSchema = recordDataType.getChildSchema();
+                tagsToOpen.addLast(fieldName);
+
+                boolean hasWritten = iterateThroughRecordUsingSchema(tagsToOpen, record, childSchema);
+
+                if (hasWritten) {
+                    writer.writeEndElement();
+                    return true;
+                } else {
+
+                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                        writeAllTags(tagsToOpen);
+                        writer.writeEndElement();
+                        return true;
+                    } else {
+                        tagsToOpen.removeLast();
+                        return false;
+                    }
+                }
+            }
+            case ARRAY: {
+                final Object[] arrayValues;
+                if (coercedValue instanceof Object[]) {
+                    arrayValues = (Object[]) coercedValue;
+                } else {
+                    arrayValues = new Object[]{coercedValue.toString()};
+                }
+
+                final ArrayDataType arrayDataType = (ArrayDataType) dataType;
+                final DataType elementType = arrayDataType.getElementType();
+
+                final String elementName;
+                final String wrapperName;
+                if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_FOR_ELEMENTS)) {
+                    elementName = arrayTagName;
+                    wrapperName = fieldName;
+                } else if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_AS_WRAPPER)) {
+                    elementName = fieldName;
+                    wrapperName = arrayTagName;
+                } else {
+                    elementName = fieldName;
+                    wrapperName = null;
+                }
+
+                if (wrapperName!= null) {
+                    tagsToOpen.addLast(wrapperName);
+                }
+
+                boolean loopHasWritten = false;
+                for (Object element : arrayValues) {
+
+                    final DataType chosenDataType = elementType.getFieldType() == RecordFieldType.CHOICE ? DataTypeUtils.chooseDataType(element, (ChoiceDataType) elementType) : elementType;
+                    final Object coercedElement = DataTypeUtils.convertType(element, chosenDataType, LAZY_DATE_FORMAT, LAZY_TIME_FORMAT, LAZY_TIMESTAMP_FORMAT, elementName);
+
+                    if (coercedElement != null) {
+                        boolean hasWritten = writeFieldForType(tagsToOpen, coercedElement, elementType, elementName);
+
+                        if (hasWritten) {
+                            loopHasWritten = true;
+                        }
+
+                    } else {
+                        if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                            writeAllTags(tagsToOpen, fieldName);
+                            writer.writeEndElement();
+                            loopHasWritten = true;
+                        }
+                    }
+                }
+
+                if (wrapperName!= null) {
+                    if (loopHasWritten) {
+                        writer.writeEndElement();
+                        return true;
+                    } else {
+                        if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                            writeAllTags(tagsToOpen);
+                            writer.writeEndElement();
+                            return true;
+                        } else {
+                            tagsToOpen.removeLast();
+                            return false;
+                        }
+                    }
+                } else {
+                    return loopHasWritten;
+                }
+            }
+            case MAP: {
+                final MapDataType mapDataType = (MapDataType) dataType;
+                final DataType valueDataType = mapDataType.getValueType();
+                final Map<String,?> map = (Map<String,?>) coercedValue;
+
+                tagsToOpen.addLast(fieldName);
+                boolean loopHasWritten = false;
+
+                for (Map.Entry<String,?> entry : map.entrySet()) {
+
+                    final String key = entry.getKey();
+
+                    final DataType chosenDataType = valueDataType.getFieldType() == RecordFieldType.CHOICE ? DataTypeUtils.chooseDataType(entry.getValue(),
+                            (ChoiceDataType) valueDataType) : valueDataType;
+                    final Object coercedElement = DataTypeUtils.convertType(entry.getValue(), chosenDataType, LAZY_DATE_FORMAT, LAZY_TIME_FORMAT, LAZY_TIMESTAMP_FORMAT, key);
+
+                    if (coercedElement != null) {
+                        boolean hasWritten = writeFieldForType(tagsToOpen, entry.getValue(), valueDataType, key);
+
+                        if (hasWritten) {
+                            loopHasWritten = true;
+                        }
+                    } else {
+                        if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                            writeAllTags(tagsToOpen, key);
+                            writer.writeEndElement();
+                            loopHasWritten = true;
+                        }
+                    }
+                }
+
+                if (loopHasWritten) {
+                    writer.writeEndElement();
+                    return true;
+                } else {
+                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                        writeAllTags(tagsToOpen);
+                        writer.writeEndElement();
+                        return true;
+                    } else {
+                        tagsToOpen.removeLast();
+                        return false;
+                    }
+                }
+            }
+            case CHOICE:
+            default: {
+                return writeUnknownField(tagsToOpen, coercedValue, fieldName);
+            }
+        }
+    }
+
+    private void writeAllTags(Deque<String> tagsToOpen, String fieldName) throws XMLStreamException {
+        tagsToOpen.addLast(fieldName);
+        writeAllTags(tagsToOpen);
+    }
+
+    private String escapeTagName(final String tagName) {
+        return TAG_NAME_CHARS_TO_STRIP.matcher(tagName).replaceAll("");
+    }
+
+    private void writeAllTags(Deque<String> tagsToOpen) throws XMLStreamException {
+        for (String tagName : tagsToOpen) {
+            writer.writeStartElement(escapeTagName(tagName));
+        }
+        tagsToOpen.clear();
+    }
+
+    @Override
+    public WriteResult writeRawRecord(Record record) throws IOException {
+
+        if (!isActiveRecordSet()) {
+            schemaAccess.writeHeader(recordSchema, getOutputStream());
+        }
+
+        checkWritingMultipleRecords();
+
+        Deque<String> tagsToOpen = new ArrayDeque<>();
+
+        try {
+            tagsToOpen.addLast(recordTagName);
+
+            boolean closingTagRequired = iterateThroughRecordWithoutSchema(tagsToOpen, record);
+            if (closingTagRequired) {
+                writer.writeEndElement();
+                hasWrittenRecord = true;
+            }
+
+        } catch (XMLStreamException e) {
+            throw new IOException(e.getMessage());
+        }
+
+        final Map<String, String> attributes = schemaAccess.getAttributes(recordSchema);
+        return WriteResult.of(incrementRecordCount(), attributes);
+    }
+
+    private boolean iterateThroughRecordWithoutSchema(Deque<String> tagsToOpen, Record record) throws XMLStreamException {
+
+        boolean loopHasWritten = false;
+
+        for (String fieldName : record.getRawFieldNames()) {
+            Object value = record.getValue(fieldName);
+
+            if (value != null) {
+                boolean hasWritten = writeUnknownField(tagsToOpen, value, fieldName);
+
+                if (hasWritten) {
+                    loopHasWritten = true;
+                }
+            } else {
+                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                    writeAllTags(tagsToOpen, fieldName);
+                    writer.writeEndElement();
+                    loopHasWritten = true;
+                }
+            }
+        }
+
+        return loopHasWritten;
+    }
+
+    private boolean writeUnknownField(Deque<String> tagsToOpen, Object value, String fieldName) throws XMLStreamException {
+
+        if (value instanceof Record) {
+            Record valueAsRecord = (Record) value;
+            tagsToOpen.addLast(fieldName);
+
+            boolean hasWritten = iterateThroughRecordWithoutSchema(tagsToOpen, valueAsRecord);
+
+            if (hasWritten) {
+                writer.writeEndElement();
+                return true;
+            } else {
+                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                    writeAllTags(tagsToOpen);
+                    writer.writeEndElement();
+                    return true;
+                } else {
+                    tagsToOpen.removeLast();
+                    return false;
+                }
+            }
+        }
+
+        if (value instanceof Object[]) {
+            Object[] valueAsArray = (Object[]) value;
+
+            final String elementName;
+            final String wrapperName;
+            if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_FOR_ELEMENTS)) {
+                elementName = arrayTagName;
+                wrapperName = fieldName;
+            } else if (arrayWrapping.equals(ArrayWrapping.USE_PROPERTY_AS_WRAPPER)) {
+                elementName = fieldName;
+                wrapperName = arrayTagName;
+            } else {
+                elementName = fieldName;
+                wrapperName = null;
+            }
+
+            if (wrapperName!= null) {
+                tagsToOpen.addLast(wrapperName);
+            }
+
+            boolean loopHasWritten = false;
+
+            for (Object element : valueAsArray) {
+                if (element != null) {
+                    boolean hasWritten = writeUnknownField(tagsToOpen, element, elementName);
+
+                    if (hasWritten) {
+                        loopHasWritten = true;
+                    }
+
+                } else {
+                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                        writeAllTags(tagsToOpen, fieldName);
+                        writer.writeEndElement();
+                        loopHasWritten = true;
+                    }
+                }
+            }
+
+            if (wrapperName!= null) {
+                if (loopHasWritten) {
+                    writer.writeEndElement();
+                    return true;
+                } else {
+                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                        writeAllTags(tagsToOpen);
+                        writer.writeEndElement();
+                        return true;
+                    } else {
+                        tagsToOpen.removeLast();
+                        return false;
+                    }
+                }
+            } else {
+                return loopHasWritten;
+            }
+        }
+
+        if (value instanceof Map) {
+            Map<String, ?> valueAsMap = (Map<String, ?>) value;
+
+            tagsToOpen.addLast(fieldName);
+            boolean loopHasWritten = false;
+
+            for (Map.Entry<String,?> entry : valueAsMap.entrySet()) {
+
+                final String key = entry.getKey();
+                final Object entryValue = entry.getValue();
+
+                if (entryValue != null) {
+                    boolean hasWritten = writeUnknownField(tagsToOpen, entry.getValue(), key);
+
+                    if (hasWritten) {
+                        loopHasWritten = true;
+                    }
+                } else {
+                    if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                        writeAllTags(tagsToOpen, key);
+                        writer.writeEndElement();
+                        loopHasWritten = true;
+                    }
+                }
+
+            }
+
+            if (loopHasWritten) {
+                writer.writeEndElement();
+                return true;
+            } else {
+                if (nullSuppression.equals(NullSuppression.NEVER_SUPPRESS) || nullSuppression.equals(NullSuppression.SUPPRESS_MISSING)) {
+                    writeAllTags(tagsToOpen);
+                    writer.writeEndElement();
+                    return true;
+                } else {
+                    tagsToOpen.removeLast();
+                    return false;
+                }
+            }
+        }
+
+        writeAllTags(tagsToOpen, fieldName);
+        writer.writeCharacters(value.toString());
+        writer.writeEndElement();
+        return true;
+    }
+
+
+    @Override
+    public String getMimeType() {
+        return "application/xml";
+    }
+
+    private boolean recordHasField(RecordField field, Record record) {
+        Set<String> recordFieldNames = record.getRawFieldNames();
+        if (recordFieldNames.contains(field.getFieldName())) {
+            return true;
+        }
+
+        for (String alias : field.getAliases()) {
+            if (recordFieldNames.contains(alias)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/xml/TestWriteXMLResult.java b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/xml/TestWriteXMLResult.java
index 13148d8..a278167 100644
--- a/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/xml/TestWriteXMLResult.java
+++ b/nifi-nar-bundles/nifi-standard-services/nifi-record-serialization-services-bundle/nifi-record-serialization-services/src/test/java/org/apache/nifi/xml/TestWriteXMLResult.java
@@ -1,1430 +1,1430 @@
-/*
- * 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.xml;
-
-import org.apache.nifi.schema.access.SchemaNameAsAttribute;
-import org.apache.nifi.serialization.SimpleRecordSchema;
-import org.apache.nifi.serialization.record.DataType;
-import org.apache.nifi.serialization.record.MapRecord;
-import org.apache.nifi.serialization.record.Record;
-import org.apache.nifi.serialization.record.RecordField;
-import org.apache.nifi.serialization.record.RecordFieldType;
-import org.apache.nifi.serialization.record.RecordSchema;
-import org.apache.nifi.serialization.record.RecordSet;
-import org.junit.Assert;
-import org.junit.Test;
-import org.xmlunit.diff.DefaultNodeMatcher;
-import org.xmlunit.diff.ElementSelectors;
-import org.xmlunit.matchers.CompareMatcher;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.math.BigInteger;
-import java.sql.Date;
-import java.sql.Time;
-import java.sql.Timestamp;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TimeZone;
-
-import static org.apache.nifi.record.NullSuppression.ALWAYS_SUPPRESS;
-import static org.apache.nifi.record.NullSuppression.NEVER_SUPPRESS;
-import static org.apache.nifi.record.NullSuppression.SUPPRESS_MISSING;
-import static org.apache.nifi.xml.ArrayWrapping.NO_WRAPPING;
-import static org.apache.nifi.xml.ArrayWrapping.USE_PROPERTY_AS_WRAPPER;
-import static org.apache.nifi.xml.ArrayWrapping.USE_PROPERTY_FOR_ELEMENTS;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.DATE_FORMAT;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.SCHEMA_IDENTIFIER_RECORD;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.TIMESTAMP_FORMAT;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.TIME_FORMAT;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getEmptyNestedRecordDefinedSchema;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getEmptyNestedRecordEmptyNestedSchema;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getEmptyRecordsWithEmptySchema;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getNestedRecords;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getNestedRecordsWithNullValues;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getNestedRecordsWithOnlyNullValues;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getRecordWithSimpleArray;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getRecordWithSimpleMap;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecords;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecordsWithChoice;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecordsWithNullValues;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecordsWithoutIdentifierInSchema;
-import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSingleRecord;
-import static org.junit.Assert.assertThat;
-
-public class TestWriteXMLResult {
-
-    @Test
-    public void testRecordNameIsNullSchemaIdentifierMissing() {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithoutIdentifierInSchema();
-
-        final String expectedMessage = "The property 'Name of Record Tag' has not been set and the writer does not find a record name in the schema.";
-        final StringBuilder actualMessage = new StringBuilder();
-
-        try {
-            new WriteXMLResult(recordSet.getSchema(), new SchemaNameAsAttribute(),
-                    out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "root", null, "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        } catch (IOException e) {
-            actualMessage.append(e.getMessage());
-        }
-        Assert.assertEquals(expectedMessage, actualMessage.toString());
-
-    }
-
-    @Test
-    public void testRecordNameIsNullSchemaIdentifierExists() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecords();
-        WriteXMLResult writer = new WriteXMLResult(recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", null, "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-
-    }
-
-    @Test
-    public void testRootNameIsNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecords();
-        WriteXMLResult writer = new WriteXMLResult(recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, null, "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        final String expectedMessage = "The writer attempts to write multiple record although property \'Name of Root Tag\' " +
-                "has not been set. If the XMLRecordSetWriter is supposed to write multiple records into one FlowFile, this property is required to be configured.";
-        final StringBuilder actualMessage = new StringBuilder();
-
-        try {
-            writer.write(recordSet);
-
-        } catch (IOException e) {
-            actualMessage.append(e.getMessage());
-        }
-        Assert.assertEquals(expectedMessage, actualMessage.toString());
-    }
-
-    @Test
-    public void testSingleRecord() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSingleRecord();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, null, "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-    }
-
-    @Test
-    public void testDataTypes() throws IOException, ParseException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        final List<RecordField> fields = new ArrayList<>();
-
-        for (final RecordFieldType fieldType : RecordFieldType.values()) {
-
-            if (fieldType == RecordFieldType.CHOICE) {
-                final List<DataType> possibleTypes = new ArrayList<>();
-                possibleTypes.add(RecordFieldType.INT.getDataType());
-                possibleTypes.add(RecordFieldType.LONG.getDataType());
-
-                fields.add(new RecordField(fieldType.name().toLowerCase(), fieldType.getChoiceDataType(possibleTypes)));
-
-            } else if (fieldType == RecordFieldType.MAP) {
-                fields.add(new RecordField(fieldType.name().toLowerCase(), fieldType.getMapDataType(RecordFieldType.INT.getDataType())));
-
-            } else {
-                fields.add(new RecordField(fieldType.name().toLowerCase(), fieldType.getDataType()));
-            }
-        }
-        final RecordSchema schema = new SimpleRecordSchema(fields, SCHEMA_IDENTIFIER_RECORD);
-
-        final DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
-        df.setTimeZone(TimeZone.getTimeZone("gmt"));
-        final long time = df.parse("2017/01/01 17:00:00.000").getTime();
-
-        final Map<String, Object> map = new LinkedHashMap<>();
-        map.put("height", 48);
-        map.put("width", 96);
-
-        final Map<String, Object> valueMap = new LinkedHashMap<>();
-        valueMap.put("string", "string");
-        valueMap.put("boolean", true);
-        valueMap.put("byte", (byte) 1);
-        valueMap.put("char", 'c');
-        valueMap.put("short", (short) 8);
-        valueMap.put("int", 9);
-        valueMap.put("bigint", BigInteger.valueOf(8L));
-        valueMap.put("long", 8L);
-        valueMap.put("float", 8.0F);
-        valueMap.put("double", 8.0D);
-        valueMap.put("decimal", 8.1D);
-        valueMap.put("date", new Date(time));
-        valueMap.put("time", new Time(time));
-        valueMap.put("timestamp", new Timestamp(time));
-        valueMap.put("record", null);
-        valueMap.put("array", null);
-        valueMap.put("choice", 48L);
-        valueMap.put("map", map);
-
-        final Record record = new MapRecord(schema, valueMap);
-        final RecordSet rs = RecordSet.of(schema, record);
-
-        WriteXMLResult writer = new WriteXMLResult( rs.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "RECORD", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(rs);
-        writer.flush();
-
-        String xmlResult = "<ROOT><RECORD><string>string</string><boolean>true</boolean><byte>1</byte><char>c</char><short>8</short>" +
-                "<int>9</int><bigint>8</bigint><long>8</long><float>8.0</float><double>8.0</double><decimal>8.1</decimal>" +
-                "<date>2017-01-01</date><time>17:00:00</time><timestamp>2017-01-01 17:00:00</timestamp><record /><choice>48</choice><array />" +
-                "<map><height>48</height><width>96</width></map></RECORD></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleRecord() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecords();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-    }
-
-    @Test
-    public void testSimpleRecordWithNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-    }
-
-    @Test
-    public void testSimpleRecordWithNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME></NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-    }
-
-    @Test
-    public void testSimpleRecordWithNullValuesSuppressMissings() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-    }
-
-    @Test
-    public void testSimpleRecordWithXMLDeclaration() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecords();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, false, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<?xml version=\"1.0\" ?><ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        Assert.assertEquals(xmlResult, out.toString().trim());
-    }
-
-    @Test
-    public void testSimpleRecordWithOutXMLDeclaration() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecords();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, false, true, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        Assert.assertEquals(xmlResult, out.toString().trim());
-    }
-
-    @Test
-    public void testEmptyRecordWithEmptySchema() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getEmptyRecordsWithEmptySchema();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
-    }
-
-    @Test
-    public void testNestedRecord() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecords();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
-                "<ADDRESS><STREET>292 West Street</STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
-                "<STREET>123 6th St.</STREET><CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testNestedRecordWithNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><ADDRESS><CITY>Jersey City</CITY></ADDRESS><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ADDRESS><CITY>Seattle</CITY></ADDRESS><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testNestedRecordWithNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
-                "<ADDRESS><STREET></STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
-                "<STREET></STREET><CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testNestedRecordWithNullValuesSuppressMissings() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
-                "<ADDRESS><STREET></STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
-                "<CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testNestedRecordWithOnlyNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testNestedRecordWithOnlyNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><ADDRESS><STREET></STREET><CITY></CITY></ADDRESS>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ADDRESS><STREET></STREET><CITY></CITY></ADDRESS>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testEmptyNestedRecordEmptySchemaNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getEmptyNestedRecordEmptyNestedSchema();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><ADDRESS></ADDRESS><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ADDRESS></ADDRESS><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testEmptyNestedRecordEmptySchemaAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getEmptyNestedRecordEmptyNestedSchema();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testNestedEmptyRecordDefinedSchemaSuppressMissing() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getEmptyNestedRecordDefinedSchema();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><ADDRESS></ADDRESS><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ADDRESS></ADDRESS><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArray() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArrayWithNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArrayWithNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArrayWithOnlyNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArrayWithOnlyNullValuesAlwaysSuppressWrapping() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testEmptyArray() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testEmptyArrayNeverSupressPropAsWrapper() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><ARRAY></ARRAY><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ARRAY></ARRAY><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArrayPropAsWrapper() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleArrayPropForElem() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_FOR_ELEMENTS, "ELEM", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleMapAlwaysSuppressWithoutNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleMapAlwaysSuppressHasNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleMapAlwaysSuppressOnlyNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleMapAlwaysSuppressEmpty() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        System.out.println(out.toString());
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleMapNeverSuppressHasNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testSimpleMapNeverSuppressEmpty() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN></CHILDREN><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN></CHILDREN><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testChoice() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithChoice();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-
-    /*
-    *
-    *
-    * Test writeRawRecord
-    *
-    *
-     */
-
-
-    @Test
-    public void testWriteWithoutSchemaSimpleRecord() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecords();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleRecordWithNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleRecordWithNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleRecordWithNullValuesSuppressMissings() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getSimpleRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaNestedRecord() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecords();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
-                "<ADDRESS><STREET>292 West Street</STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
-                "<STREET>123 6th St.</STREET><CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaNestedRecordWithNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
-                "<ADDRESS><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
-                "<CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaNestedRecordWithNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-        RecordSet recordSet = getNestedRecordsWithNullValues();
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
-                "<ADDRESS><STREET></STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
-                "<CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaNestedRecordWithOnlyNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaNestedRecordWithOnlyNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><ADDRESS><STREET></STREET><CITY></CITY></ADDRESS>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ADDRESS></ADDRESS>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArray() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArrayWithNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArrayWithNullValuesNeverSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArrayWithOnlyNullValuesAlwaysSuppress() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArrayWithOnlyNullValuesAlwaysSuppressWrapping() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaEmptyArray() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaEmptyArrayNeverSupressPropAsWrapper() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><ARRAY></ARRAY><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ARRAY></ARRAY><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArrayPropAsWrapper() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleArrayPropForElem() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_FOR_ELEMENTS, "ELEM", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressWithoutNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressHasNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.write(recordSet);
-        writer.flush();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressOnlyNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressEmpty() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleMapNeverSuppressHasNull() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
-                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
-                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-
-    @Test
-    public void testWriteWithoutSchemaSimpleMapNeverSuppressEmpty() throws IOException {
-        OutputStream out = new ByteArrayOutputStream();
-
-        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
-
-        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
-                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
-
-        writer.onBeginRecordSet();
-
-        Record record;
-        while ((record = recordSet.next()) != null) {
-            writer.writeRawRecord(record);
-        }
-
-        writer.onFinishRecordSet();
-        writer.flush();
-        writer.close();
-
-        String xmlResult = "<ROOT><PERSON><CHILDREN></CHILDREN><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
-                "<PERSON><CHILDREN></CHILDREN><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
-
-        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
-    }
-}
+/*
+ * 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.xml;
+
+import org.apache.nifi.schema.access.SchemaNameAsAttribute;
+import org.apache.nifi.serialization.SimpleRecordSchema;
+import org.apache.nifi.serialization.record.DataType;
+import org.apache.nifi.serialization.record.MapRecord;
+import org.apache.nifi.serialization.record.Record;
+import org.apache.nifi.serialization.record.RecordField;
+import org.apache.nifi.serialization.record.RecordFieldType;
+import org.apache.nifi.serialization.record.RecordSchema;
+import org.apache.nifi.serialization.record.RecordSet;
+import org.junit.Assert;
+import org.junit.Test;
+import org.xmlunit.diff.DefaultNodeMatcher;
+import org.xmlunit.diff.ElementSelectors;
+import org.xmlunit.matchers.CompareMatcher;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.sql.Date;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+import static org.apache.nifi.record.NullSuppression.ALWAYS_SUPPRESS;
+import static org.apache.nifi.record.NullSuppression.NEVER_SUPPRESS;
+import static org.apache.nifi.record.NullSuppression.SUPPRESS_MISSING;
+import static org.apache.nifi.xml.ArrayWrapping.NO_WRAPPING;
+import static org.apache.nifi.xml.ArrayWrapping.USE_PROPERTY_AS_WRAPPER;
+import static org.apache.nifi.xml.ArrayWrapping.USE_PROPERTY_FOR_ELEMENTS;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.DATE_FORMAT;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.SCHEMA_IDENTIFIER_RECORD;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.TIMESTAMP_FORMAT;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.TIME_FORMAT;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getEmptyNestedRecordDefinedSchema;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getEmptyNestedRecordEmptyNestedSchema;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getEmptyRecordsWithEmptySchema;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getNestedRecords;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getNestedRecordsWithNullValues;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getNestedRecordsWithOnlyNullValues;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getRecordWithSimpleArray;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getRecordWithSimpleMap;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecords;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecordsWithChoice;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecordsWithNullValues;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSimpleRecordsWithoutIdentifierInSchema;
+import static org.apache.nifi.xml.TestWriteXMLResultUtils.getSingleRecord;
+import static org.junit.Assert.assertThat;
+
+public class TestWriteXMLResult {
+
+    @Test
+    public void testRecordNameIsNullSchemaIdentifierMissing() {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithoutIdentifierInSchema();
+
+        final String expectedMessage = "The property 'Name of Record Tag' has not been set and the writer does not find a record name in the schema.";
+        final StringBuilder actualMessage = new StringBuilder();
+
+        try {
+            new WriteXMLResult(recordSet.getSchema(), new SchemaNameAsAttribute(),
+                    out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "root", null, "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        } catch (IOException e) {
+            actualMessage.append(e.getMessage());
+        }
+        Assert.assertEquals(expectedMessage, actualMessage.toString());
+
+    }
+
+    @Test
+    public void testRecordNameIsNullSchemaIdentifierExists() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecords();
+        WriteXMLResult writer = new WriteXMLResult(recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", null, "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+
+    }
+
+    @Test
+    public void testRootNameIsNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecords();
+        WriteXMLResult writer = new WriteXMLResult(recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, null, "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        final String expectedMessage = "The writer attempts to write multiple record although property \'Name of Root Tag\' " +
+                "has not been set. If the XMLRecordSetWriter is supposed to write multiple records into one FlowFile, this property is required to be configured.";
+        final StringBuilder actualMessage = new StringBuilder();
+
+        try {
+            writer.write(recordSet);
+
+        } catch (IOException e) {
+            actualMessage.append(e.getMessage());
+        }
+        Assert.assertEquals(expectedMessage, actualMessage.toString());
+    }
+
+    @Test
+    public void testSingleRecord() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSingleRecord();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, null, "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+    }
+
+    @Test
+    public void testDataTypes() throws IOException, ParseException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        final List<RecordField> fields = new ArrayList<>();
+
+        for (final RecordFieldType fieldType : RecordFieldType.values()) {
+
+            if (fieldType == RecordFieldType.CHOICE) {
+                final List<DataType> possibleTypes = new ArrayList<>();
+                possibleTypes.add(RecordFieldType.INT.getDataType());
+                possibleTypes.add(RecordFieldType.LONG.getDataType());
+
+                fields.add(new RecordField(fieldType.name().toLowerCase(), fieldType.getChoiceDataType(possibleTypes)));
+
+            } else if (fieldType == RecordFieldType.MAP) {
+                fields.add(new RecordField(fieldType.name().toLowerCase(), fieldType.getMapDataType(RecordFieldType.INT.getDataType())));
+
+            } else {
+                fields.add(new RecordField(fieldType.name().toLowerCase(), fieldType.getDataType()));
+            }
+        }
+        final RecordSchema schema = new SimpleRecordSchema(fields, SCHEMA_IDENTIFIER_RECORD);
+
+        final DateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss.SSS");
+        df.setTimeZone(TimeZone.getTimeZone("gmt"));
+        final long time = df.parse("2017/01/01 17:00:00.000").getTime();
+
+        final Map<String, Object> map = new LinkedHashMap<>();
+        map.put("height", 48);
+        map.put("width", 96);
+
+        final Map<String, Object> valueMap = new LinkedHashMap<>();
+        valueMap.put("string", "string");
+        valueMap.put("boolean", true);
+        valueMap.put("byte", (byte) 1);
+        valueMap.put("char", 'c');
+        valueMap.put("short", (short) 8);
+        valueMap.put("int", 9);
+        valueMap.put("bigint", BigInteger.valueOf(8L));
+        valueMap.put("long", 8L);
+        valueMap.put("float", 8.0F);
+        valueMap.put("double", 8.0D);
+        valueMap.put("decimal", 8.1D);
+        valueMap.put("date", new Date(time));
+        valueMap.put("time", new Time(time));
+        valueMap.put("timestamp", new Timestamp(time));
+        valueMap.put("record", null);
+        valueMap.put("array", null);
+        valueMap.put("choice", 48L);
+        valueMap.put("map", map);
+
+        final Record record = new MapRecord(schema, valueMap);
+        final RecordSet rs = RecordSet.of(schema, record);
+
+        WriteXMLResult writer = new WriteXMLResult( rs.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "RECORD", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(rs);
+        writer.flush();
+
+        String xmlResult = "<ROOT><RECORD><string>string</string><boolean>true</boolean><byte>1</byte><char>c</char><short>8</short>" +
+                "<int>9</int><bigint>8</bigint><long>8</long><float>8.0</float><double>8.0</double><decimal>8.1</decimal>" +
+                "<date>2017-01-01</date><time>17:00:00</time><timestamp>2017-01-01 17:00:00</timestamp><record /><choice>48</choice><array />" +
+                "<map><height>48</height><width>96</width></map></RECORD></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleRecord() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecords();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+    }
+
+    @Test
+    public void testSimpleRecordWithNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+    }
+
+    @Test
+    public void testSimpleRecordWithNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME></NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+    }
+
+    @Test
+    public void testSimpleRecordWithNullValuesSuppressMissings() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+    }
+
+    @Test
+    public void testSimpleRecordWithXMLDeclaration() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecords();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, false, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<?xml version=\"1.0\" ?><ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        Assert.assertEquals(xmlResult, out.toString().trim());
+    }
+
+    @Test
+    public void testSimpleRecordWithOutXMLDeclaration() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecords();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, false, true, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        Assert.assertEquals(xmlResult, out.toString().trim());
+    }
+
+    @Test
+    public void testEmptyRecordWithEmptySchema() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getEmptyRecordsWithEmptySchema();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isIdenticalTo(out.toString()).ignoreWhitespace());
+    }
+
+    @Test
+    public void testNestedRecord() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecords();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
+                "<ADDRESS><STREET>292 West Street</STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
+                "<STREET>123 6th St.</STREET><CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testNestedRecordWithNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><ADDRESS><CITY>Jersey City</CITY></ADDRESS><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ADDRESS><CITY>Seattle</CITY></ADDRESS><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testNestedRecordWithNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
+                "<ADDRESS><STREET></STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
+                "<STREET></STREET><CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testNestedRecordWithNullValuesSuppressMissings() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
+                "<ADDRESS><STREET></STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
+                "<CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testNestedRecordWithOnlyNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testNestedRecordWithOnlyNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><ADDRESS><STREET></STREET><CITY></CITY></ADDRESS>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ADDRESS><STREET></STREET><CITY></CITY></ADDRESS>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testEmptyNestedRecordEmptySchemaNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getEmptyNestedRecordEmptyNestedSchema();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><ADDRESS></ADDRESS><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ADDRESS></ADDRESS><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testEmptyNestedRecordEmptySchemaAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getEmptyNestedRecordEmptyNestedSchema();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testNestedEmptyRecordDefinedSchemaSuppressMissing() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getEmptyNestedRecordDefinedSchema();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><ADDRESS></ADDRESS><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ADDRESS></ADDRESS><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArray() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArrayWithNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArrayWithNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArrayWithOnlyNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArrayWithOnlyNullValuesAlwaysSuppressWrapping() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testEmptyArray() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testEmptyArrayNeverSupressPropAsWrapper() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><ARRAY></ARRAY><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ARRAY></ARRAY><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArrayPropAsWrapper() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleArrayPropForElem() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_FOR_ELEMENTS, "ELEM", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleMapAlwaysSuppressWithoutNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleMapAlwaysSuppressHasNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleMapAlwaysSuppressOnlyNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleMapAlwaysSuppressEmpty() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        System.out.println(out.toString());
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleMapNeverSuppressHasNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testSimpleMapNeverSuppressEmpty() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN></CHILDREN><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN></CHILDREN><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testChoice() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithChoice();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+
+    /*
+    *
+    *
+    * Test writeRawRecord
+    *
+    *
+     */
+
+
+    @Test
+    public void testWriteWithoutSchemaSimpleRecord() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecords();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleRecordWithNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleRecordWithNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleRecordWithNullValuesSuppressMissings() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getSimpleRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, SUPPRESS_MISSING, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME></NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaNestedRecord() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecords();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
+                "<ADDRESS><STREET>292 West Street</STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
+                "<STREET>123 6th St.</STREET><CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaNestedRecordWithNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
+                "<ADDRESS><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
+                "<CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaNestedRecordWithNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+        RecordSet recordSet = getNestedRecordsWithNullValues();
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY>" +
+                "<ADDRESS><STREET></STREET><CITY>Jersey City</CITY></ADDRESS></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY><ADDRESS>" +
+                "<CITY>Seattle</CITY></ADDRESS></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaNestedRecordWithOnlyNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaNestedRecordWithOnlyNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getNestedRecordsWithOnlyNullValues();
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><ADDRESS><STREET></STREET><CITY></CITY></ADDRESS>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ADDRESS></ADDRESS>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArray() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArrayWithNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArrayWithNullValuesNeverSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN>Tom</CHILDREN><CHILDREN></CHILDREN><CHILDREN>Ben</CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArrayWithOnlyNullValuesAlwaysSuppress() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArrayWithOnlyNullValuesAlwaysSuppressWrapping() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaEmptyArray() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaEmptyArrayNeverSupressPropAsWrapper() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><ARRAY></ARRAY><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ARRAY></ARRAY><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArrayPropAsWrapper() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_AS_WRAPPER, "ARRAY", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><ARRAY><CHILDREN>Tom</CHILDREN><CHILDREN>Anna</CHILDREN><CHILDREN>Ben</CHILDREN></ARRAY>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleArrayPropForElem() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleArray(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, USE_PROPERTY_FOR_ELEMENTS, "ELEM", "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><ELEM>Tom</ELEM><ELEM>Anna</ELEM><ELEM>Ben</ELEM></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressWithoutNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.WITHOUT_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2>Anna</CHILD2></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressHasNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.write(recordSet);
+        writer.flush();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressOnlyNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.ONLY_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleMapAlwaysSuppressEmpty() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, ALWAYS_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleMapNeverSuppressHasNull() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.HAS_NULL);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
+                "<NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN><CHILD1>Tom</CHILD1><CHILD3>Ben</CHILD3><CHILD2></CHILD2></CHILDREN>" +
+                "<NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+
+    @Test
+    public void testWriteWithoutSchemaSimpleMapNeverSuppressEmpty() throws IOException {
+        OutputStream out = new ByteArrayOutputStream();
+
+        RecordSet recordSet = getRecordWithSimpleMap(TestWriteXMLResultUtils.NullValues.EMPTY);
+
+        WriteXMLResult writer = new WriteXMLResult( recordSet.getSchema(), new SchemaNameAsAttribute(),
+                out, true, false, NEVER_SUPPRESS, NO_WRAPPING, null, "ROOT", "PERSON", "UTF-8", DATE_FORMAT, TIME_FORMAT, TIMESTAMP_FORMAT);
+
+        writer.onBeginRecordSet();
+
+        Record record;
+        while ((record = recordSet.next()) != null) {
+            writer.writeRawRecord(record);
+        }
+
+        writer.onFinishRecordSet();
+        writer.flush();
+        writer.close();
+
+        String xmlResult = "<ROOT><PERSON><CHILDREN></CHILDREN><NAME>Cleve Butler</NAME><AGE>42</AGE><COUNTRY>USA</COUNTRY></PERSON>" +
+                "<PERSON><CHILDREN></CHILDREN><NAME>Ainslie Fletcher</NAME><AGE>33</AGE><COUNTRY>UK</COUNTRY></PERSON></ROOT>";
+
+        assertThat(xmlResult, CompareMatcher.isSimilarTo(out.toString()).ignoreWhitespace().withNodeMatcher(new DefaultNodeMatcher(ElementSelectors.byNameAndText)));
+    }
+}


[nifi] 07/11: NIFI-7768 Added support for monogdb+srv connection strings.

Posted by al...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

alopresto pushed a commit to branch support/nifi-1.12.x
in repository https://gitbox.apache.org/repos/asf/nifi.git

commit 2174af7a3ad7de47f355a7e509d02a09100991af
Author: Mike Thomsen <mt...@apache.org>
AuthorDate: Mon Sep 7 20:08:46 2020 -0400

    NIFI-7768 Added support for monogdb+srv connection strings.
    
    This closes #4514.
    
    Signed-off-by: Joey Frazee <jf...@apache.org>
---
 .../apache/nifi/processors/mongodb/GetMongoIT.java |  9 +++++----
 .../processors/mongodb/RunMongoAggregationIT.java  |  9 +++++++--
 nifi-nar-bundles/nifi-mongodb-bundle/pom.xml       | 23 +---------------------
 3 files changed, 13 insertions(+), 28 deletions(-)

diff --git a/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/GetMongoIT.java b/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/GetMongoIT.java
index ddea9a8..7ec724c 100644
--- a/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/GetMongoIT.java
+++ b/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/GetMongoIT.java
@@ -311,11 +311,12 @@ public class GetMongoIT {
          * Test original behavior; Manually set query of {}, no input
          */
         final String attr = "query.attr";
-        runner.setProperty(GetMongo.QUERY, "{}");
+        final String queryValue = "{}";
+        runner.setProperty(GetMongo.QUERY, queryValue);
         runner.setProperty(GetMongo.QUERY_ATTRIBUTE, attr);
         runner.run();
         runner.assertTransferCount(GetMongo.REL_SUCCESS, 3);
-        testQueryAttribute(attr, "{ }");
+        testQueryAttribute(attr, queryValue);
 
         runner.clearTransferState();
 
@@ -325,7 +326,7 @@ public class GetMongoIT {
         runner.removeProperty(GetMongo.QUERY);
         runner.setIncomingConnection(false);
         runner.run();
-        testQueryAttribute(attr, "{ }");
+        testQueryAttribute(attr, queryValue);
 
         runner.clearTransferState();
 
@@ -336,7 +337,7 @@ public class GetMongoIT {
         runner.setIncomingConnection(true);
         runner.enqueue("{}");
         runner.run();
-        testQueryAttribute(attr, "{ }");
+        testQueryAttribute(attr, queryValue);
 
         /*
          * Input flowfile with invalid query
diff --git a/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/RunMongoAggregationIT.java b/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/RunMongoAggregationIT.java
index 2aadc5f..24c130f 100644
--- a/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/RunMongoAggregationIT.java
+++ b/nifi-nar-bundles/nifi-mongodb-bundle/nifi-mongodb-processors/src/test/java/org/apache/nifi/processors/mongodb/RunMongoAggregationIT.java
@@ -37,6 +37,7 @@ import org.junit.Test;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.Calendar;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -239,12 +240,16 @@ public class RunMongoAggregationIT {
 
     @Test
     public void testExtendedJsonSupport() throws Exception {
-        String pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS";
+        String pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'";
         SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern);
+        //Let's put this a week from now to make sure that we're not getting too close to
+        //the creation date
+        Date nowish = new Date(now.getTime().getTime() + (7 * 24 * 60 * 60 * 1000));
+
         final String queryInput = "[\n" +
             "  {\n" +
             "    \"$match\": {\n" +
-            "      \"date\": { \"$gte\": { \"$date\": \"2019-01-01T00:00:00Z\" }, \"$lte\": { \"$date\": \"" + simpleDateFormat.format(now.getTime()) + "\" } }\n" +
+            "      \"date\": { \"$gte\": { \"$date\": \"2019-01-01T00:00:00Z\" }, \"$lte\": { \"$date\": \"" + simpleDateFormat.format(nowish) + "\" } }\n" +
             "    }\n" +
             "  },\n" +
             "  {\n" +
diff --git a/nifi-nar-bundles/nifi-mongodb-bundle/pom.xml b/nifi-nar-bundles/nifi-mongodb-bundle/pom.xml
index ba46fde..1f2d7ad 100644
--- a/nifi-nar-bundles/nifi-mongodb-bundle/pom.xml
+++ b/nifi-nar-bundles/nifi-mongodb-bundle/pom.xml
@@ -35,30 +35,9 @@
     </modules>
 
     <properties>
-        <mongo.driver.version>3.2.2</mongo.driver.version>
+        <mongo.driver.version>3.12.7</mongo.driver.version>
     </properties>
 
-    <profiles>
-        <profile>
-            <id>3.4</id>
-            <properties>
-                <mongo.driver.version>3.4.3</mongo.driver.version>
-            </properties>
-        </profile>
-        <profile>
-            <id>3.6</id>
-            <properties>
-                <mongo.driver.version>3.6.4</mongo.driver.version>
-            </properties>
-        </profile>
-        <profile>
-            <id>3.8</id>
-            <properties>
-                <mongo.driver.version>3.8.0</mongo.driver.version>
-            </properties>
-        </profile>
-    </profiles>
-
     <dependencyManagement>
         <dependencies>
             <dependency>