You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by sl...@apache.org on 2023/02/01 17:30:55 UTC

[daffodil] branch main updated: Allow to preserve whitespace in XML text content

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

slawrence pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil.git


The following commit(s) were added to refs/heads/main by this push:
     new 10c520aef Allow <![CDATA[]]> to preserve whitespace in XML text content
10c520aef is described below

commit 10c520aefa3e0911586da24447d499a7e5c28f9a
Author: Varun Zaver <vz...@owlcyberdefense.com>
AuthorDate: Mon Aug 1 09:10:01 2022 -0500

    Allow <![CDATA[]]> to preserve whitespace in XML text content
    
    Add a new constructor parameter (an enum XMLTextEscapeStyle with
    Standard and CDATA as values) to XMLTextInfosetOutputter. When CDATA is
    passed instead of Standard, wrap simple XML elements' text contents in
    CDATA brackets to preserve any whitespace they contain.
    
    DAFFODIL-2346
---
 .../daffodil/japi/infoset/XMLTextEscapeStyle.java  |  37 ++++++
 .../org/apache/daffodil/japi/infoset/Infoset.scala |  24 +++-
 .../daffodil/japi/packageprivate/Utils.scala       |  15 +++
 .../org/apache/daffodil/example/TestJavaAPI.java   |  89 ++++++++++++-
 .../resources/test/japi/mySchemaCDATA.dfdl.xsd     |  41 ++++++
 .../daffodil/infoset/XMLTextEscapeStyle.scala      |  33 +++++
 .../daffodil/infoset/XMLTextInfosetOutputter.scala |  28 +++-
 .../org/apache/daffodil/sapi/infoset/Infoset.scala |  15 ++-
 .../daffodil/sapi/infoset/XMLTextEscapeStyle.scala |  33 +++++
 .../daffodil/sapi/packageprivate/Utils.scala       |  14 ++
 .../resources/test/sapi/mySchemaCDATA.dfdl.xsd     |  41 ++++++
 .../org/apache/daffodil/example/TestScalaAPI.scala | 146 +++++++++++++++------
 .../validation/schematron/EmbeddedTesting.scala    |   3 +-
 13 files changed, 460 insertions(+), 59 deletions(-)

diff --git a/daffodil-japi/src/main/java/org/apache/daffodil/japi/infoset/XMLTextEscapeStyle.java b/daffodil-japi/src/main/java/org/apache/daffodil/japi/infoset/XMLTextEscapeStyle.java
new file mode 100644
index 000000000..b7bfa8b52
--- /dev/null
+++ b/daffodil-japi/src/main/java/org/apache/daffodil/japi/infoset/XMLTextEscapeStyle.java
@@ -0,0 +1,37 @@
+/*
+ * 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.daffodil.japi.infoset;
+
+/**
+ * XMLTextEscapeStyles for determining whether to wrap info in CDATA tags
+ */
+public enum XMLTextEscapeStyle {
+  /**
+   * Special characters (quotation mark, ampersand, less-than, greater-than) in the
+   * text of xs:string elements are escaped, while non-special characters are written
+   * as is.
+   */
+  Standard,
+
+  /**
+   * The text of xs:string elements are wrapped in CDATA tags if the string contains
+   * special characters (quotation mark, ampersand, less-than, greater-than) or
+   * whitespace
+   */
+  CDATA,
+}
diff --git a/daffodil-japi/src/main/scala/org/apache/daffodil/japi/infoset/Infoset.scala b/daffodil-japi/src/main/scala/org/apache/daffodil/japi/infoset/Infoset.scala
index 3ccfbb22e..62a5c02d8 100644
--- a/daffodil-japi/src/main/scala/org/apache/daffodil/japi/infoset/Infoset.scala
+++ b/daffodil-japi/src/main/scala/org/apache/daffodil/japi/infoset/Infoset.scala
@@ -40,6 +40,8 @@ import org.apache.daffodil.infoset.DIComplex
 import org.apache.daffodil.infoset.DIArray
 import org.apache.daffodil.dpath.NodeInfo
 
+import org.apache.daffodil.japi.packageprivate._
+
 /**
  * Abstract class used to determine how the infoset representation should be
  * input from a call to DataProcessor#unparse. This uses a Cursor API, such
@@ -52,7 +54,7 @@ abstract class InfosetInputter extends SInfosetInputter {
   /**
    * Return the current infoset inputter event type
    */
-  def getEventType(): InfosetInputterEventType 
+  def getEventType(): InfosetInputterEventType
 
   /**
    * Get the local name of the current event. This will only be called when the
@@ -211,7 +213,7 @@ abstract class InfosetOutputter extends SInfosetOutputter {
  * classes, we can document these classes and have a small and clean javadoc.
  */
 
- 
+
 /**
  * [[InfosetOutputter]] to build an infoset represented as a scala.xml.Node
  *
@@ -244,7 +246,23 @@ class XMLTextInfosetOutputter private (outputter: SXMLTextInfosetOutputter)
    *               insert indentation and newlines where it will not affect the
    *               content of the XML.
    */
-  def this(os: java.io.OutputStream, pretty: Boolean) = this(new SXMLTextInfosetOutputter(os, pretty))
+  def this(os: java.io.OutputStream, pretty: Boolean) = this(new SXMLTextInfosetOutputter(os,
+    pretty, XMLTextEscapeStyleConversions.styleToScala(XMLTextEscapeStyle.Standard)))
+
+  /**
+   * Output the infoset as XML Text, written to a java.io.OutputStream
+   *
+   * @param os the java.io.OutputStream to write the XML text to
+   * @param pretty enable or disable pretty printing. Pretty printing will only
+   *               insert indentation and newlines where it will not affect the
+   *               content of the XML.
+   * @param xmlTextEscapeStyle determine whether to wrap values of elements of type
+   *                       xs:string in CDATA tags in order to preserve
+   *                       whitespace.
+   */
+  def this(os: java.io.OutputStream, pretty: Boolean, xmlTextEscapeStyle: XMLTextEscapeStyle) = {
+    this(new SXMLTextInfosetOutputter(os, pretty, XMLTextEscapeStyleConversions.styleToScala(xmlTextEscapeStyle)))
+  }
 
   override val infosetOutputter = outputter
 }
diff --git a/daffodil-japi/src/main/scala/org/apache/daffodil/japi/packageprivate/Utils.scala b/daffodil-japi/src/main/scala/org/apache/daffodil/japi/packageprivate/Utils.scala
index b18ef28a9..14aae3364 100644
--- a/daffodil-japi/src/main/scala/org/apache/daffodil/japi/packageprivate/Utils.scala
+++ b/daffodil-japi/src/main/scala/org/apache/daffodil/japi/packageprivate/Utils.scala
@@ -30,6 +30,9 @@ import org.apache.daffodil.api.{ ValidationMode => SValidationMode }
 import org.apache.daffodil.debugger.{ InteractiveDebugger => SInteractiveDebugger }
 import org.apache.daffodil.debugger.{ InteractiveDebuggerRunner => SInteractiveDebuggerRunner }
 
+import org.apache.daffodil.infoset.{ XMLTextEscapeStyle => SXMLTextEscapeStyle }
+import org.apache.daffodil.japi.infoset._
+
 private[japi] object ValidationConversions {
 
   def modeToScala(mode: ValidationMode): SValidationMode.Type = {
@@ -42,6 +45,18 @@ private[japi] object ValidationConversions {
   }
 }
 
+private[japi] object XMLTextEscapeStyleConversions {
+
+  def styleToScala(style: XMLTextEscapeStyle): SXMLTextEscapeStyle.Value = {
+    val sxmlTextEscapeStyle: SXMLTextEscapeStyle.Value = style match {
+      case XMLTextEscapeStyle.Standard => SXMLTextEscapeStyle.Standard
+      case XMLTextEscapeStyle.CDATA => SXMLTextEscapeStyle.CDATA
+      case _ => throw new Exception("Unrecognized value: %s for parameter: xmlTextEscapeStyle. Must be 'Standard' or 'CDATA'.".format(style))
+    }
+    sxmlTextEscapeStyle
+  }
+}
+
 /* A wrapper interctive debugger that scala debugging can talk to, which is
  * then forwarded onto the java interactive debugger, if a user implements
  * their own debugger in java.
diff --git a/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java b/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java
index 8b34d0d77..d975a59ed 100644
--- a/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java
+++ b/daffodil-japi/src/test/java/org/apache/daffodil/example/TestJavaAPI.java
@@ -53,6 +53,9 @@ import org.xml.sax.XMLReader;
 
 import javax.xml.XMLConstants;
 
+import java.nio.charset.StandardCharsets;
+import org.apache.daffodil.japi.infoset.*;
+
 public class TestJavaAPI {
 
     /**
@@ -262,7 +265,7 @@ public class TestJavaAPI {
 
     /**
      * Verify that we can detect when the parse did not consume all the data.
-     * 
+     *
      * @throws IOException
      */
     @Test
@@ -393,7 +396,7 @@ public class TestJavaAPI {
     /***
      * Verify that the compiler throws a FileNotFound exception when fed a list
      * of schema files that do not exist.
-     * 
+     *
      * @throws IOException
      */
     @Test
@@ -415,7 +418,7 @@ public class TestJavaAPI {
     /**
      * Tests a user submitted case where the XML appears to be serializing odd
      * xml entities into the output.
-     * 
+     *
      * @throws IOException
      */
     @Test
@@ -452,7 +455,7 @@ public class TestJavaAPI {
      * that this test uses double newline as a terminator for the first element
      * in the sequence rather than double newline as a separator for the
      * sequence
-     * 
+     *
      * @throws IOException
      */
     @Test
@@ -931,7 +934,6 @@ public class TestJavaAPI {
         assertEquals("42", unparseBos.toString());
     }
 
-
     @Test
     public void testJavaAPI21() throws IOException, ClassNotFoundException {
         // Test SAX parsing with errors
@@ -1213,4 +1215,81 @@ public class TestJavaAPI {
         assertTrue(DaffodilXMLEntityResolver.getXMLEntityResolver() != null);
         assertTrue(DaffodilXMLEntityResolver.getLSResourceResolver() != null);
     }
+
+    @Test
+    public void testJavaAPINullXMLTextEscapeStyle() throws IOException, ClassNotFoundException {
+        ByteArrayOutputStream xmlBos = new ByteArrayOutputStream();
+        try {
+            XMLTextInfosetOutputter outputter = new XMLTextInfosetOutputter(xmlBos, true, null);
+        } catch (Exception e) {
+            String msg = e.getMessage().toLowerCase();
+            assertTrue(msg.contains("unrecognized"));
+            assertTrue(msg.contains("null"));
+            assertTrue(msg.contains("xmltextescapestyle"));
+        }
+    }
+
+    @Test
+    public void testJavaAPICDATA1() throws IOException, ClassNotFoundException {
+        String expected = "NO_WHITESPACE_OR_SPECIAL_CHARS";
+        String data = "NO_WHITESPACE_OR_SPECIAL_CHARS$";
+        String schemaType = "string";
+        doXMLTextEscapeStyleTest(expected, data, schemaType);
+    }
+
+    @Test
+    public void testJavaAPICDATA2() throws IOException, ClassNotFoundException {
+        String expected = "<![CDATA[   'some' stuff   here &#xE000; and ]]]]><![CDATA[> even]]>";
+        String data = "   'some' stuff   here &#xE000; and ]]> even$";
+        String schemaType = "string";
+        doXMLTextEscapeStyleTest(expected, data, schemaType);
+    }
+
+    @Test
+    public void testJavaAPICDATA3() throws IOException, ClassNotFoundException {
+        String expected = "6.892";
+        String data = "6.892";
+        String schemaType = "float";
+        doXMLTextEscapeStyleTest(expected, data, schemaType);
+    }
+
+    @Test
+    public void testJavaAPICDATA4() throws IOException, ClassNotFoundException {
+        String expected = "<![CDATA[this contains a CRLF\nline ending]]>";
+        String data = "this contains a CRLF\r\nline ending$";
+        String schemaType = "string";
+        doXMLTextEscapeStyleTest(expected, data, schemaType);
+    }
+
+    @Test
+    public void testJavaAPICDATA5() throws IOException, ClassNotFoundException {
+        String expected = "<![CDATA[abcd&gt]]>";
+        String data = "abcd&gt$";
+        String schemaType = "string";
+        doXMLTextEscapeStyleTest(expected, data, schemaType);
+    }
+
+    public void doXMLTextEscapeStyleTest(String expect, String data, String schemaType)
+        throws IOException, ClassNotFoundException {
+
+        org.apache.daffodil.japi.Compiler c = Daffodil.compiler();
+        java.io.File schemaFile = getResource("/test/japi/mySchemaCDATA.dfdl.xsd");
+        ProcessorFactory pf = c.compileFile(schemaFile, schemaType, null);
+        DataProcessor dp = pf.onPath("/");
+
+        ByteArrayInputStream is = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8));
+        InputSourceDataInputStream input = new InputSourceDataInputStream(is);
+        ByteArrayOutputStream bosDP = new ByteArrayOutputStream();
+        XMLTextInfosetOutputter outputter = new XMLTextInfosetOutputter(bosDP, true, XMLTextEscapeStyle.CDATA);
+        ParseResult res = dp.parse(input, outputter);
+        boolean err = res.isError();
+
+        String infosetDPString = bosDP.toString();
+        int start = infosetDPString.indexOf(".com\">") + 6;
+        int end = infosetDPString.indexOf("</tns");
+        String value = infosetDPString.substring(start, end);
+
+        assertFalse(err);
+        assertEquals(expect, value);
+    }
 }
diff --git a/daffodil-japi/src/test/resources/test/japi/mySchemaCDATA.dfdl.xsd b/daffodil-japi/src/test/resources/test/japi/mySchemaCDATA.dfdl.xsd
new file mode 100644
index 000000000..fad71923a
--- /dev/null
+++ b/daffodil-japi/src/test/resources/test/japi/mySchemaCDATA.dfdl.xsd
@@ -0,0 +1,41 @@
+<?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
+  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.
+-->
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+  targetNamespace="http://example.com" xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/"
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tns="http://example.com">
+
+  <annotation>
+    <appinfo source="http://www.ogf.org/dfdl/">
+      <dfdl:format ref="tns:GeneralFormat" />
+    </appinfo>
+  </annotation>
+
+  <include schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+
+  <element name="string" type="xsd:string" dfdl:lengthKind="delimited" dfdl:terminator="$"/>
+  <element name="float" type="xsd:float"
+    dfdl:representation="text"
+    dfdl:textNumberRep="standard"
+    dfdl:lengthKind="delimited"
+    dfdl:encoding="UTF-8"
+    dfdl:textNumberPattern="0.000"
+    dfdl:textStandardDecimalSeparator="." />
+
+</schema>
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextEscapeStyle.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextEscapeStyle.scala
new file mode 100644
index 000000000..58041ceff
--- /dev/null
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextEscapeStyle.scala
@@ -0,0 +1,33 @@
+/*
+ * 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.daffodil.infoset
+/**
+ * XMLTextEscapeStyle determines how to wrap values of elements of type xs:string
+ *
+ * Standard - Special characters (quotation mark, ampersand, less-than, greater-than)
+ * in the text of xs:string elements are escaped, while non-special characters are
+ * written as is.
+ *
+ * CDATA - The text of xs:string elements are wrapped in CDATA tags if the string
+ * contains whitespace or special characters (quotation mark, ampersand, less-than,
+ * greater-than)
+ */
+object XMLTextEscapeStyle extends Enumeration {
+  type XMLTextEscapeStyle = Value
+  val Standard, CDATA = Value
+}
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextInfosetOutputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextInfosetOutputter.scala
index 54e1a647d..145a0bf55 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextInfosetOutputter.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/XMLTextInfosetOutputter.scala
@@ -31,12 +31,16 @@ import org.apache.daffodil.util.Indentable
  * @param writer The writer to write the XML text to
  * @param pretty Whether or to enable pretty printing. Set to true, XML
  *               elements are indented and newlines are inserted.
+ * @param xmlTextEscapeStyle Determine whether to wrap values of elements of type
+ *        xs:string in CDATA tags in order to preserve whitespace.
  */
-class XMLTextInfosetOutputter private (writer: java.io.Writer, pretty: Boolean)
+class XMLTextInfosetOutputter private (writer: java.io.Writer, pretty: Boolean,
+  xmlTextEscapeStyle: XMLTextEscapeStyle.Value)
   extends InfosetOutputter with Indentable with XMLInfosetOutputter {
 
-  def this(os: java.io.OutputStream, pretty: Boolean) = {
-    this(new java.io.OutputStreamWriter(os, StandardCharsets.UTF_8), pretty)
+  def this(os: java.io.OutputStream, pretty: Boolean,
+    xmlTextEscapeStyle: XMLTextEscapeStyle.Value = XMLTextEscapeStyle.Standard) = {
+    this(new java.io.OutputStreamWriter(os, StandardCharsets.UTF_8), pretty, xmlTextEscapeStyle)
   }
 
   private val sb = new StringBuilder()
@@ -160,7 +164,21 @@ class XMLTextInfosetOutputter private (writer: java.io.Writer, pretty: Boolean)
         if (simple.erd.runtimeProperties.get(XMLTextInfoset.stringAsXml) == "true") {
           writeStringAsXml(simpleVal)
         } else {
-          writer.write(scala.xml.Utility.escape(remapped(simpleVal)))
+          val xmlSafe = remapped(simpleVal)
+          val escaped = xmlTextEscapeStyle match {
+            case XMLTextEscapeStyle.CDATA => {
+              val needsCDataEscape = xmlSafe.exists { c =>
+                scala.xml.Utility.Escapes.escMap.contains(c) || c.isWhitespace
+              }
+              if (needsCDataEscape) {
+                "<![CDATA[%s]]>".format(xmlSafe.replaceAll("]]>", "]]]]><![CDATA[>"))
+              } else {
+                xmlSafe
+              }
+            }
+            case XMLTextEscapeStyle.Standard => scala.xml.Utility.escape(xmlSafe)
+          }
+          writer.write(escaped)
         }
       } else {
         writer.write(simple.dataValueAsString)
@@ -170,7 +188,7 @@ class XMLTextInfosetOutputter private (writer: java.io.Writer, pretty: Boolean)
     outputEndTag(simple)
     inScopeComplexElementHasChildren = true
   }
-  
+
   override def endSimple(simple: DISimple): Unit = {
     // do nothing, everything is done in startSimple
   }
diff --git a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/Infoset.scala b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/Infoset.scala
index 667804746..95c20d535 100644
--- a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/Infoset.scala
+++ b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/Infoset.scala
@@ -40,6 +40,8 @@ import org.apache.daffodil.infoset.DIComplex
 import org.apache.daffodil.infoset.DIArray
 import org.apache.daffodil.dpath.NodeInfo
 
+import org.apache.daffodil.sapi.packageprivate._
+
 /**
  * Abstract class used to determine how the infoset representation should be
  * input from a call to [[DataProcessor.unparse(input* DataProcessor.unparse]]. This uses a Cursor API, such
@@ -52,7 +54,7 @@ abstract class InfosetInputter extends SInfosetInputter {
   /**
    * Return the current infoset inputter event type
    */
-  def getEventType(): InfosetInputterEventType 
+  def getEventType(): InfosetInputterEventType
 
   /**
    * Get the local name of the current event. This will only be called when the
@@ -216,7 +218,7 @@ abstract class InfosetOutputter extends SInfosetOutputter {
  * classes, we can document these classes and have a small and clean scaladoc.
  */
 
- 
+
 /**
  * [[InfosetOutputter]] to build an infoset represented as a scala.xml.Node
  *
@@ -248,8 +250,15 @@ class XMLTextInfosetOutputter private (outputter: SXMLTextInfosetOutputter)
    * @param pretty enable or disable pretty printing. Pretty printing will only
    *               insert indentation and newlines where it will not affect the
    *               content of the XML.
+   * @param xmlTextEscapeStyle determine whether to wrap values of elements of type
+   *                       xs:string in CDATA tags in order to preserve
+   *                       whitespace.
    */
-  def this(os: java.io.OutputStream, pretty: Boolean) = this(new SXMLTextInfosetOutputter(os, pretty))
+  def this(os: java.io.OutputStream, pretty: Boolean,
+    xmlTextEscapeStyle: XMLTextEscapeStyle.Value = XMLTextEscapeStyle.Standard) = {
+    this(new SXMLTextInfosetOutputter(os, pretty,
+    XMLTextEscapeStyleConversions.styleToScala(xmlTextEscapeStyle)))
+  }
 
   override val infosetOutputter = outputter
 }
diff --git a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/XMLTextEscapeStyle.scala b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/XMLTextEscapeStyle.scala
new file mode 100644
index 000000000..07a36bf6a
--- /dev/null
+++ b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/infoset/XMLTextEscapeStyle.scala
@@ -0,0 +1,33 @@
+/*
+ * 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.daffodil.sapi.infoset
+/**
+ * XMLTextEscapeStyle determines how to wrap values of elements of type xs:string
+ *
+ * Standard - Special characters (quotation mark, ampersand, less-than, greater-than)
+ * in the text of xs:string elements are escaped, while non-special characters are
+ * written as is.
+ *
+ * CDATA - The text of xs:string elements are wrapped in CDATA tags if the string
+ * contains whitespace or special characters (quotation mark, ampersand, less-than,
+ * greater-than)
+ */
+object XMLTextEscapeStyle extends Enumeration {
+  type XMLTextEscapeStyle = Value
+  val Standard, CDATA = Value
+}
diff --git a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/packageprivate/Utils.scala b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/packageprivate/Utils.scala
index 0bdcd29aa..085e64634 100644
--- a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/packageprivate/Utils.scala
+++ b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/packageprivate/Utils.scala
@@ -30,6 +30,9 @@ import org.apache.daffodil.api.{ ValidationMode => SValidationMode }
 import org.apache.daffodil.debugger.{ InteractiveDebugger => SInteractiveDebugger }
 import org.apache.daffodil.debugger.{ InteractiveDebuggerRunner => SInteractiveDebuggerRunner }
 
+import org.apache.daffodil.infoset.{ XMLTextEscapeStyle => SXMLTextEscapeStyle }
+import org.apache.daffodil.sapi.infoset._
+
 private[sapi] object ValidationConversions {
 
   def modeToScala(mode: ValidationMode.Value): SValidationMode.Type = {
@@ -53,6 +56,17 @@ private[sapi] object ValidationConversions {
   }
 }
 
+private[sapi] object XMLTextEscapeStyleConversions {
+
+  def styleToScala(style: XMLTextEscapeStyle.Value): SXMLTextEscapeStyle.Value = {
+    val sxmlTextEscapeStyle: SXMLTextEscapeStyle.Value = style match {
+      case XMLTextEscapeStyle.Standard => SXMLTextEscapeStyle.Standard
+      case XMLTextEscapeStyle.CDATA => SXMLTextEscapeStyle.CDATA
+    }
+    sxmlTextEscapeStyle
+  }
+}
+
 /* A wrapper interctive debugger that scala debugging can talk to, which is
  * then forwarded onto the java interactive debugger, if a user implements
  * their own debugger in java.
diff --git a/daffodil-sapi/src/test/resources/test/sapi/mySchemaCDATA.dfdl.xsd b/daffodil-sapi/src/test/resources/test/sapi/mySchemaCDATA.dfdl.xsd
new file mode 100644
index 000000000..fad71923a
--- /dev/null
+++ b/daffodil-sapi/src/test/resources/test/sapi/mySchemaCDATA.dfdl.xsd
@@ -0,0 +1,41 @@
+<?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
+  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.
+-->
+
+<schema xmlns="http://www.w3.org/2001/XMLSchema"
+  targetNamespace="http://example.com" xmlns:dfdl="http://www.ogf.org/dfdl/dfdl-1.0/"
+  xmlns:xsd="http://www.w3.org/2001/XMLSchema"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tns="http://example.com">
+
+  <annotation>
+    <appinfo source="http://www.ogf.org/dfdl/">
+      <dfdl:format ref="tns:GeneralFormat" />
+    </appinfo>
+  </annotation>
+
+  <include schemaLocation="org/apache/daffodil/xsd/DFDLGeneralFormat.dfdl.xsd"/>
+
+  <element name="string" type="xsd:string" dfdl:lengthKind="delimited" dfdl:terminator="$"/>
+  <element name="float" type="xsd:float"
+    dfdl:representation="text"
+    dfdl:textNumberRep="standard"
+    dfdl:lengthKind="delimited"
+    dfdl:encoding="UTF-8"
+    dfdl:textNumberPattern="0.000"
+    dfdl:textStandardDecimalSeparator="." />
+
+</schema>
diff --git a/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala b/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala
index 394fe4b9b..c4195ef35 100644
--- a/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala
+++ b/daffodil-sapi/src/test/scala/org/apache/daffodil/example/TestScalaAPI.scala
@@ -54,6 +54,8 @@ import org.apache.daffodil.sapi.infoset.ScalaXMLInfosetOutputter
 import org.apache.daffodil.sapi.infoset.XMLTextInfosetOutputter
 import org.apache.daffodil.sapi.io.InputSourceDataInputStream
 
+import java.nio.charset.StandardCharsets
+import org.apache.daffodil.sapi.infoset.XMLTextEscapeStyle
 
 object TestScalaAPI {
   /**
@@ -934,7 +936,7 @@ class TestScalaAPI {
 
     val saxUr = unparseContentHandler.getUnparseResult
     wbc.close()
-    
+
     val saxErr = saxUr.isError()
     assertFalse(saxErr)
     assertTrue(saxUr.getDiagnostics.isEmpty)
@@ -1127,44 +1129,106 @@ class TestScalaAPI {
   }
 
 
-    @Test
-    def testScalaAPI25(): Unit = {
-      // Demonstrates the use of a custom InfosetInputter/Outputter
-
-      val expectedData = "42"
-      val expectedEvents = Array(
-        TestInfosetEvent.startDocument(),
-        TestInfosetEvent.startComplex("e1", "http://example.com"),
-        TestInfosetEvent.startSimple("e2", "http://example.com", expectedData),
-        TestInfosetEvent.endSimple("e2", "http://example.com"),
-        TestInfosetEvent.endComplex("e1", "http://example.com"),
-        TestInfosetEvent.endDocument()
-      )
-
-      val c = Daffodil.compiler()
-
-      val schemaFile = getResource("/test/sapi/mySchema1.dfdl.xsd")
-      val pf = c.compileFile(schemaFile)
-      val dp = pf.onPath("/")
-
-      val file = getResource("/test/sapi/myData.dat")
-      val fis = new java.io.FileInputStream(file)
-      val dis = new InputSourceDataInputStream(fis)
-      val outputter = new TestInfosetOutputter()
-      val pr = dp.parse(dis, outputter)
-
-      assertFalse(pr.isError())
-      assertArrayEquals(
-        expectedEvents.asInstanceOf[Array[Object]],
-        outputter.events.toArray.asInstanceOf[Array[Object]]
-      )
-
-      val bos = new java.io.ByteArrayOutputStream()
-      val wbc = java.nio.channels.Channels.newChannel(bos)
-      val inputter = new TestInfosetInputter(expectedEvents: _*)
-
-      val ur = dp.unparse(inputter, wbc)
-      assertFalse(ur.isError)
-      assertEquals(expectedData, bos.toString())
-    }
+  @Test
+  def testScalaAPI25(): Unit = {
+    // Demonstrates the use of a custom InfosetInputter/Outputter
+
+    val expectedData = "42"
+    val expectedEvents = Array(
+      TestInfosetEvent.startDocument(),
+      TestInfosetEvent.startComplex("e1", "http://example.com"),
+      TestInfosetEvent.startSimple("e2", "http://example.com", expectedData),
+      TestInfosetEvent.endSimple("e2", "http://example.com"),
+      TestInfosetEvent.endComplex("e1", "http://example.com"),
+      TestInfosetEvent.endDocument()
+    )
+
+    val c = Daffodil.compiler()
+
+    val schemaFile = getResource("/test/sapi/mySchema1.dfdl.xsd")
+    val pf = c.compileFile(schemaFile)
+    val dp = pf.onPath("/")
+
+    val file = getResource("/test/sapi/myData.dat")
+    val fis = new java.io.FileInputStream(file)
+    val dis = new InputSourceDataInputStream(fis)
+    val outputter = new TestInfosetOutputter()
+    val pr = dp.parse(dis, outputter)
+
+    assertFalse(pr.isError())
+    assertArrayEquals(
+      expectedEvents.asInstanceOf[Array[Object]],
+      outputter.events.toArray.asInstanceOf[Array[Object]]
+    )
+
+    val bos = new java.io.ByteArrayOutputStream()
+    val wbc = java.nio.channels.Channels.newChannel(bos)
+    val inputter = new TestInfosetInputter(expectedEvents: _*)
+
+    val ur = dp.unparse(inputter, wbc)
+    assertFalse(ur.isError)
+    assertEquals(expectedData, bos.toString())
+  }
+
+  @Test
+  def testScalaAPICDATA1(): Unit = {
+    val expected = "NO_WHITESPACE_OR_SPECIAL_CHARS"
+    val data = "NO_WHITESPACE_OR_SPECIAL_CHARS$"
+    val schemaType = "string"
+    doXMLTextEscapeStyleTest(expected, data, schemaType)
+  }
+
+  @Test
+  def testScalaAPICDATA2(): Unit = {
+    val expected = "<![CDATA[   'some' stuff   here &#xE000; and ]]]]><![CDATA[> even]]>"
+    val data = "   'some' stuff   here &#xE000; and ]]> even$"
+    val schemaType = "string"
+    doXMLTextEscapeStyleTest(expected, data, schemaType)
+  }
+
+  @Test
+  def testScalaAPICDATA3(): Unit = {
+    val expected = "6.892"
+    val data = "6.892"
+    val schemaType = "float"
+    doXMLTextEscapeStyleTest(expected, data, schemaType)
+  }
+
+  @Test
+  def testScalaAPICDATA4(): Unit = {
+    val expected = "<![CDATA[this contains a CRLF\nline ending]]>"
+    val data = "this contains a CRLF\r\nline ending$"
+    val schemaType = "string"
+    doXMLTextEscapeStyleTest(expected, data, schemaType)
+  }
+
+  @Test
+  def testScalaAPICDATA5(): Unit = {
+    val expected = "<![CDATA[abcd&gt]]>"
+    val data = "abcd&gt$"
+    val schemaType = "string"
+    doXMLTextEscapeStyleTest(expected, data, schemaType)
+  }
+
+  def doXMLTextEscapeStyleTest(expect: String, data: String, schemaType: String): Unit = {
+    val c = Daffodil.compiler()
+    val schemaFile = getResource("/test/sapi/mySchemaCDATA.dfdl.xsd")
+    val pf = c.compileFile(schemaFile, Some(schemaType), None)
+    var dp = pf.onPath("/")
+
+    val is = new ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))
+    val input = new InputSourceDataInputStream(is)
+    val bosDP = new ByteArrayOutputStream()
+    val outputter = new XMLTextInfosetOutputter(bosDP, true, XMLTextEscapeStyle.CDATA)
+    val res = dp.parse(input, outputter)
+    val err = res.isError()
+
+    val infosetDPString = bosDP.toString()
+    val start = infosetDPString.indexOf(".com\">") + 6
+    val end = infosetDPString.indexOf("</tns")
+    val value = infosetDPString.substring(start, end)
+
+    assertFalse(err)
+    assertEquals(expect, value)
+  }
 }
diff --git a/daffodil-schematron/src/test/scala/org/apache/daffodil/validation/schematron/EmbeddedTesting.scala b/daffodil-schematron/src/test/scala/org/apache/daffodil/validation/schematron/EmbeddedTesting.scala
index 6a914eb72..182e4109d 100644
--- a/daffodil-schematron/src/test/scala/org/apache/daffodil/validation/schematron/EmbeddedTesting.scala
+++ b/daffodil-schematron/src/test/scala/org/apache/daffodil/validation/schematron/EmbeddedTesting.scala
@@ -52,8 +52,7 @@ trait EmbeddedTesting {
       val bos = new ByteArrayOutputStream()
       val r1 = dp.parse(
         new InputSourceDataInputStream(new ByteArrayInputStream(bytes)),
-        new XMLTextInfosetOutputter(bos, true))
-
+        new XMLTextInfosetOutputter(bos, pretty = true))
       verbose match {
         case Always | AnyError if r1.isError() => r1.getDiagnostics.foreach(println)
         case Always => println(bos.toString)