You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@daffodil.apache.org by ol...@apache.org on 2020/09/15 18:06:53 UTC

[incubator-daffodil] branch master updated: Support for SAX Parsing

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

olabusayo pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-daffodil.git


The following commit(s) were added to refs/heads/master by this push:
     new 8628820  Support for SAX Parsing
8628820 is described below

commit 86288209122994fa7d1de4a39574e286a3f354d0
Author: olabusayoT <50...@users.noreply.github.com>
AuthorDate: Thu Aug 6 12:40:40 2020 -0400

    Support for SAX Parsing
    
    - add sax option to command line
    - add DaffodilXMLReader instance to DataProcessor
    - implement DaffodilXMLReader, DaffodilOutputContentHandler and
    SAXInfosetOutputter
    - updates cached validator to re-add features, handlers and resolvers
    - adds SAX Parsing/Testing to TDMLRunner
    - adds SAX TDML Parser implementation to DaffofilTDMLProcessor
    - update getInfosetOutputter to return SAXHandler in the case of sax,
    and other outputters as appropriate
    - Added setBlobAttribute for SAXInfosetOutputters using Blob properties
    - Added parseresult as Property on xmlReader
    - Updated optPrefix to have same implementation as getPrefix
    - Updated TDML to compare SAX ouput and data processor output
    - Added checkPrefixes and checkNamespaces flag to computeDiff and
    compareReport
    - Added DaffodilXMLReader to DFDL
    - implement DaffodilOutputContentHandler and replaced SAXHandler
    (DAFFODIL-2389)
    - removed thisElementNamepace/Prefix, infavor of namedQName
    - Added SAX Parsing implementation to Scala/Java APIs with tests
    - created Error Handler for TDML
    - Add scala/java API documentation
    - Fixed bug in JSONInfosetOutputter that was causing additional
    diagnostic for legacy parsing
    
    DAFFODIL-2382
---
 .../src/main/scala/org/apache/daffodil/Main.scala  | 215 +++++++++------
 .../org/apache/daffodil/dsom/ElementBase.scala     |   5 +-
 .../runtime1/ElementBaseRuntime1Mixin.scala        |   2 -
 .../daffodil/dsom/TestSimpleTypeUnions.scala       |  44 +--
 .../scala/org/apache/daffodil/util/TestUtils.scala |   1 +
 .../org/apache/daffodil/japi/package-info.java     | 139 +++++++++-
 .../scala/org/apache/daffodil/japi/Daffodil.scala  | 126 +++++++++
 .../org/apache/daffodil/example/TestJavaAPI.java   |  92 ++++++-
 .../daffodil/japi/SAXErrorHandlerForJAPITest.java  |  60 ++++
 .../scala/org/apache/daffodil/util/Validator.scala |  49 ++--
 .../scala/org/apache/daffodil/xml/QNameBase.scala  |  25 +-
 .../scala/org/apache/daffodil/xml/XMLUtils.scala   |  78 +++++-
 .../apache/daffodil/api/DFDLParserUnparser.scala   |  12 +-
 .../daffodil/infoset/JDOMInfosetOutputter.scala    |   6 +-
 .../daffodil/infoset/JsonInfosetOutputter.scala    |   2 +-
 .../daffodil/infoset/SAXInfosetOutputter.scala     | 303 +++++++++++++++++++++
 .../infoset/ScalaXMLInfosetOutputter.scala         |  20 +-
 .../daffodil/infoset/W3CDOMInfosetOutputter.scala  |   4 +-
 .../daffodil/infoset/XMLTextInfosetOutputter.scala |   2 +-
 .../apache/daffodil/processors/DataProcessor.scala | 193 ++++++++++++-
 .../apache/daffodil/processors/RuntimeData.scala   |  14 +-
 .../scala/org/apache/daffodil/sapi/Daffodil.scala  | 125 ++++++++-
 .../scala/org/apache/daffodil/sapi/package.scala   | 119 +++++++-
 .../org/apache/daffodil/example/TestScalaAPI.scala |  92 ++++++-
 .../daffodil/sapi/SAXErrorHandlerForSAPITest.scala |  47 ++++
 .../org/apache/daffodil/tdml/TDMLRunner.scala      |  79 +++---
 .../tdml/processor/DaffodilTDMLDFDLProcessor.scala | 135 ++++++++-
 .../daffodil/section07/variables/variables.tdml    |   2 +-
 28 files changed, 1728 insertions(+), 263 deletions(-)

diff --git a/daffodil-cli/src/main/scala/org/apache/daffodil/Main.scala b/daffodil-cli/src/main/scala/org/apache/daffodil/Main.scala
index 4e148d8..2aa2b1b 100644
--- a/daffodil-cli/src/main/scala/org/apache/daffodil/Main.scala
+++ b/daffodil-cli/src/main/scala/org/apache/daffodil/Main.scala
@@ -17,84 +17,89 @@
 
 package org.apache.daffodil
 
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.FileInputStream
 import java.io.FileOutputStream
 import java.io.OutputStream
-import java.io.FileInputStream
-import java.io.ByteArrayInputStream
+import java.net.URI
+import java.nio.ByteBuffer
 import java.nio.channels.Channels
 import java.nio.file.Paths
 import java.util.Scanner
-
-import scala.xml.{SAXParseException, Node}
-import org.rogach.scallop
-import org.apache.daffodil.debugger.{InteractiveDebugger, CLIDebuggerRunner, TraceDebuggerRunner}
-import org.apache.daffodil.util.Misc
-import org.apache.daffodil.util.Timer
-import org.apache.daffodil.xml._
-import org.apache.daffodil.exceptions.Assert
-import org.apache.daffodil.compiler.Compiler
-import org.apache.daffodil.api.{WithDiagnostics, URISchemaSource, DFDL, DaffodilTunables}
-import org.apache.daffodil.util.Logging
-import org.apache.daffodil.util.LogLevel
-import org.apache.daffodil.util.LogWriter
-import org.apache.daffodil.util.LoggingDefaults
-import org.apache.daffodil.exceptions.NotYetImplementedException
-import java.io.File
-
-import org.apache.daffodil.tdml.DFDLTestSuite
-import org.apache.daffodil.externalvars.{Binding, BindingException}
-import org.apache.daffodil.externalvars.ExternalVariablesLoader
-import org.apache.daffodil.configuration.ConfigurationLoader
-import org.apache.daffodil.api.ValidationMode
-
-import scala.language.reflectiveCalls
-import scala.concurrent.Future
 import java.util.concurrent.Executors
 
+import scala.concurrent.Await
 import scala.concurrent.ExecutionContext
-import org.rogach.scallop.ScallopOption
-
+import scala.concurrent.Future
 import scala.concurrent.duration.Duration
-import scala.concurrent.Await
-import org.apache.daffodil.xml.QName
-import org.apache.daffodil.dsom.ExpressionCompilers
-import org.apache.daffodil.compiler.InvalidParserException
-import java.net.URI
+import scala.language.reflectiveCalls
+import scala.xml.Node
+import scala.xml.SAXParseException
 
-import org.apache.daffodil.tdml.TDMLException
-import org.apache.daffodil.xml.RefQName
-import org.rogach.scallop.ArgType
-import org.rogach.scallop.ValueConverter
-import org.apache.daffodil.processors.DataProcessor
-import org.apache.daffodil.processors.DataLoc
-import org.apache.daffodil.processors.HasSetDebugger
+import javax.xml.parsers.DocumentBuilderFactory
+import javax.xml.transform.TransformerFactory
+import javax.xml.transform.dom.DOMSource
+import javax.xml.transform.stream.StreamResult
+import org.apache.commons.io.IOUtils
+import org.apache.daffodil.api.DFDL
+import org.apache.daffodil.api.DFDL.ParseResult
+import org.apache.daffodil.api.DaffodilTunables
+import org.apache.daffodil.api.URISchemaSource
+import org.apache.daffodil.api.ValidationMode
+import org.apache.daffodil.api.WithDiagnostics
+import org.apache.daffodil.compiler.Compiler
+import org.apache.daffodil.compiler.InvalidParserException
+import org.apache.daffodil.configuration.ConfigurationLoader
+import org.apache.daffodil.debugger.CLIDebuggerRunner
+import org.apache.daffodil.debugger.InteractiveDebugger
+import org.apache.daffodil.debugger.TraceDebuggerRunner
+import org.apache.daffodil.dsom.ExpressionCompilers
+import org.apache.daffodil.exceptions.Assert
+import org.apache.daffodil.exceptions.NotYetImplementedException
 import org.apache.daffodil.exceptions.UnsuppressableException
-import org.apache.daffodil.infoset.XMLTextInfosetOutputter
-import org.apache.daffodil.infoset.NullInfosetOutputter
-import org.apache.daffodil.infoset.ScalaXMLInfosetOutputter
-import org.apache.daffodil.infoset.JsonInfosetOutputter
+import org.apache.daffodil.externalvars.Binding
+import org.apache.daffodil.externalvars.BindingException
+import org.apache.daffodil.externalvars.ExternalVariablesLoader
+import org.apache.daffodil.infoset.DaffodilOutputContentHandler
+import org.apache.daffodil.infoset.InfosetInputter
 import org.apache.daffodil.infoset.InfosetOutputter
+import org.apache.daffodil.infoset.JDOMInfosetInputter
 import org.apache.daffodil.infoset.JDOMInfosetOutputter
-import org.apache.daffodil.infoset.W3CDOMInfosetOutputter
-import org.apache.daffodil.infoset.XMLTextInfosetInputter
 import org.apache.daffodil.infoset.JsonInfosetInputter
+import org.apache.daffodil.infoset.JsonInfosetOutputter
+import org.apache.daffodil.infoset.NullInfosetOutputter
 import org.apache.daffodil.infoset.ScalaXMLInfosetInputter
-import org.apache.daffodil.infoset.JDOMInfosetInputter
+import org.apache.daffodil.infoset.ScalaXMLInfosetOutputter
 import org.apache.daffodil.infoset.W3CDOMInfosetInputter
-import org.apache.daffodil.infoset.InfosetInputter
-import javax.xml.transform.TransformerFactory
-import javax.xml.transform.dom.DOMSource
-import javax.xml.transform.stream.StreamResult
-import javax.xml.parsers.DocumentBuilderFactory
-import org.apache.commons.io.IOUtils
-import org.apache.daffodil.io.InputSourceDataInputStream
-import org.apache.daffodil.tdml.TDMLTestNotCompatibleException
+import org.apache.daffodil.infoset.W3CDOMInfosetOutputter
+import org.apache.daffodil.infoset.XMLTextInfosetInputter
+import org.apache.daffodil.infoset.XMLTextInfosetOutputter
 import org.apache.daffodil.io.DataDumper
-import java.nio.ByteBuffer
-
 import org.apache.daffodil.io.FormatInfo
+import org.apache.daffodil.io.InputSourceDataInputStream
+import org.apache.daffodil.processors.DataLoc
+import org.apache.daffodil.processors.DataProcessor
+import org.apache.daffodil.processors.HasSetDebugger
 import org.apache.daffodil.schema.annotation.props.gen.BitOrder
+import org.apache.daffodil.tdml.DFDLTestSuite
+import org.apache.daffodil.tdml.TDMLException
+import org.apache.daffodil.tdml.TDMLTestNotCompatibleException
 import org.apache.daffodil.udf.UserDefinedFunctionFatalErrorException
+import org.apache.daffodil.util.LogLevel
+import org.apache.daffodil.util.LogWriter
+import org.apache.daffodil.util.Logging
+import org.apache.daffodil.util.LoggingDefaults
+import org.apache.daffodil.util.Misc
+import org.apache.daffodil.util.Timer
+import org.apache.daffodil.xml.QName
+import org.apache.daffodil.xml.RefQName
+import org.apache.daffodil.xml.DaffodilXMLLoader
+import org.apache.daffodil.xml.XMLUtils
+import org.rogach.scallop
+import org.rogach.scallop.ArgType
+import org.rogach.scallop.ScallopOption
+import org.rogach.scallop.ValueConverter
 
 class NullOutputStream extends OutputStream {
   override def close(): Unit = {
@@ -114,14 +119,14 @@ class NullOutputStream extends OutputStream {
   }
 }
 
-class CommandLineXMLLoaderErrorHandler() extends org.xml.sax.ErrorHandler with Logging {
+class CommandLineSAXErrorHandler() extends org.xml.sax.ErrorHandler with Logging {
 
   def warning(exception: SAXParseException) = {
-    log(LogLevel.Warning, "loading schema: " + exception.getMessage())
+    log(LogLevel.Warning, exception.getMessage())
   }
 
   def error(exception: SAXParseException) = {
-    log(LogLevel.Error, "loading schema: " + exception.getMessage())
+    log(LogLevel.Error, exception.getMessage())
     System.exit(1)
   }
 
@@ -337,7 +342,7 @@ class CLIConf(arguments: Array[String]) extends scallop.ScallopConf(arguments)
     val vars = props[String]('D', keyName = "variable", valueName = "value", descr = "variables to be used when parsing. An optional namespace may be provided.")
     val tunables = props[String]('T', keyName = "tunable", valueName = "value", descr = "daffodil tunable to be used when parsing.")
     val config = opt[String](short = 'c', argName = "file", descr = "path to file containing configuration items.")
-    val infosetType = opt[String](short = 'I', argName = "infoset_type", descr = "infoset type to output. Must be one of 'xml', 'scala-xml', 'json', 'jdom', 'w3cdom', or 'null'.", default = Some("xml")).map { _.toLowerCase }
+    val infosetType = opt[String](short = 'I', argName = "infoset_type", descr = "infoset type to output. Must be one of 'xml', 'scala-xml', 'json', 'jdom', 'w3cdom', 'sax', or 'null'.", default = Some("xml")).map { _.toLowerCase }
     val stream = toggle(noshort = true, default = Some(false), descrYes = "when left over data exists, parse again with remaining data, separating infosets by a NUL character", descrNo = "stop after the first parse, even if left over data exists")
     val infile = trailArg[String](required = false, descr = "input file to parse. If not specified, or a value of -, reads from stdin.")
 
@@ -364,6 +369,7 @@ class CLIConf(arguments: Array[String]) extends scallop.ScallopConf(arguments)
       case (Some("json")) => Right(Unit)
       case (Some("jdom")) => Right(Unit)
       case (Some("w3cdom")) => Right(Unit)
+      case (Some("sax")) => Right(Unit)
       case (Some("null")) => Right(Unit)
       case (Some(t)) => Left("Unknown infoset type: " + t)
       case _ => Assert.impossible() // not possible due to default value
@@ -401,7 +407,7 @@ class CLIConf(arguments: Array[String]) extends scallop.ScallopConf(arguments)
     val vars = props[String]('D', keyName = "variable", valueName = "value", descr = "variables to be used when processing. An optional namespace may be provided.")
     val tunables = props[String]('T', keyName = "tunable", valueName = "value", descr = "daffodil tunable to be used when processing.")
     val config = opt[String](short = 'c', argName = "file", descr = "path to file containing configuration items.")
-    val infosetType = opt[String](short = 'I', argName = "infoset_type", descr = "infoset type to parse/unparse. Must be one of 'xml', 'scala-xml', 'json', 'jdom', 'w3cdom', or 'null'.", default = Some("xml")).map { _.toLowerCase }
+    val infosetType = opt[String](short = 'I', argName = "infoset_type", descr = "infoset type to parse/unparse. Must be one of 'xml', 'scala-xml', 'json', 'jdom', 'w3cdom', 'sax', or 'null'.", default = Some("xml")).map { _.toLowerCase }
     val infile = trailArg[String](required = true, descr = "input file or directory containing files to process.")
 
     validateOpt(schema, parser, rootNS) {
@@ -417,6 +423,8 @@ class CLIConf(arguments: Array[String]) extends scallop.ScallopConf(arguments)
       case (Some("json"), _) => Right(Unit)
       case (Some("jdom"), _) => Right(Unit)
       case (Some("w3cdom"), _) => Right(Unit)
+      case (Some("sax"), Some(true)) => Left("SAX unparse is not yet implemented")
+      case (Some("sax"), _) => Right(Unit)
       case (Some("null"), Some(true)) => Left("infoset type null not valid with performance --unparse")
       case (Some("null"), _) => Right(Unit)
       case (Some(t), _) => Left("Unknown infoset type: " + t)
@@ -482,6 +490,7 @@ class CLIConf(arguments: Array[String]) extends scallop.ScallopConf(arguments)
       case (Some("json")) => Right(Unit)
       case (Some("jdom")) => Right(Unit)
       case (Some("w3cdom")) => Right(Unit)
+      case (Some("sax")) => Right(Unit)
       //case (Some("null")) => Right(Unit) // null is not valid for unparsing
       case (Some(t)) => Left("Unknown infoset type: " + t)
       case _ => Assert.impossible() // not possible due to default value
@@ -723,16 +732,23 @@ object Main extends Logging {
   val blobDir = Paths.get(System.getProperty("user.dir"), "daffodil-blobs")
   val blobSuffix = ".bin"
 
-  def getInfosetOutputter(infosetType: String, os: java.io.OutputStream): InfosetOutputter = {
+  def getInfosetOutputter(infosetType: String, os: java.io.OutputStream)
+  : Either[InfosetOutputter, DaffodilOutputContentHandler] = {
     val outputter = infosetType match {
-      case "xml" => new XMLTextInfosetOutputter(os, true)
-      case "scala-xml" => new ScalaXMLInfosetOutputter()
-      case "json" => new JsonInfosetOutputter(os, true)
-      case "jdom" => new JDOMInfosetOutputter()
-      case "w3cdom" => new W3CDOMInfosetOutputter()
-      case "null" => new NullInfosetOutputter()
+      case "xml" => Left(new XMLTextInfosetOutputter(os, pretty = true))
+      case "scala-xml" => Left(new ScalaXMLInfosetOutputter())
+      case "json" => Left(new JsonInfosetOutputter(os, pretty = true))
+      case "jdom" => Left(new JDOMInfosetOutputter())
+      case "w3cdom" => Left(new W3CDOMInfosetOutputter())
+      case "sax" => Right(new DaffodilOutputContentHandler(os, pretty = true))
+      case "null" => Left(new NullInfosetOutputter())
+    }
+    if (outputter.isLeft) {
+      outputter.left.map(_.setBlobAttributes(blobDir, null, blobSuffix))
+    } else {
+      // do nothing here, we set the blobAttributes using the Blob* properties
+      // within the sax parse calls
     }
-    outputter.setBlobAttributes(blobDir, null, blobSuffix)
     outputter
   }
 
@@ -840,7 +856,8 @@ object Main extends Logging {
               case Some("-") | None => System.out
               case Some(file) => new FileOutputStream(file)
             }
-            val outputter = getInfosetOutputter(parseOpts.infosetType.toOption.get, output)
+            val infosetType = parseOpts.infosetType.toOption.get
+            val eitherOutputterOrHandler = getInfosetOutputter(infosetType, output)
 
             var lastParseBitPosition = 0L
             var keepParsing = true
@@ -848,9 +865,17 @@ object Main extends Logging {
 
             while (keepParsing) {
 
-              outputter.reset() // reset in case we are streaming
-
-              val parseResult = Timer.getResult("parsing", processor.parse(inStream, outputter))
+              val parseResult = eitherOutputterOrHandler match {
+                case Right(saxContentHandler) =>
+                  // reset in case we are streaming
+                  saxContentHandler.reset()
+                  Timer.getResult("parsing",
+                    parseWithSAX(processor, inStream, saxContentHandler,
+                    new CommandLineSAXErrorHandler()))
+                case Left(outputter) =>
+                  outputter.reset() // reset in case we are streaming
+                  Timer.getResult("parsing", processor.parse(inStream, outputter))
+              }
               val finfo = parseResult.resultState.asInstanceOf[FormatInfo]
               val loc = parseResult.resultState.currentLocation.asInstanceOf[DataLoc]
               displayDiagnostics(parseResult)
@@ -859,19 +884,19 @@ object Main extends Logging {
                 keepParsing = false
                 error = true
               } else {
-                // only XMLTextInfosetOutputter and JsonInfosetOutputter write
-                // directly to the output stream. Other InfosetOutputters must manually
-                // get the result and write it to the stream
-                outputter match {
-                  case sxml: ScalaXMLInfosetOutputter => {
+                // only XMLTextInfosetOutputter, JsonInfosetOutputter and
+                // DaffodilOutputContentHandler write directly to the output stream. Other
+                // InfosetOutputters must manually get the result and write it to the stream below
+                eitherOutputterOrHandler match {
+                  case Left(sxml: ScalaXMLInfosetOutputter) => {
                     val writer = new java.io.OutputStreamWriter(output, "UTF-8")
                     scala.xml.XML.write(writer, sxml.getResult, "UTF-8", true, null)
                     writer.flush()
                   }
-                  case jdom: JDOMInfosetOutputter => {
+                  case Left(jdom: JDOMInfosetOutputter) => {
                     new org.jdom2.output.XMLOutputter().output(jdom.getResult, output)
                   }
-                  case w3cdom: W3CDOMInfosetOutputter => {
+                  case Left(w3cdom: W3CDOMInfosetOutputter) => {
                     val tf = TransformerFactory.newInstance()
                     val transformer = tf.newTransformer()
                     val result = new StreamResult(output)
@@ -1037,8 +1062,15 @@ object Main extends Logging {
                       })
                       case Right(data) => Timer.getTimeResult({
                         val input = InputSourceDataInputStream(data)
-                        val outputterForParse = getInfosetOutputter(infosetType, nullOutputStreamForParse)
-                        processor.parse(input, outputterForParse)
+                        val eitherOutputterOrHandlerForParse = getInfosetOutputter(infosetType, nullOutputStreamForParse)
+                        eitherOutputterOrHandlerForParse match {
+                          case Left(outputter) => processor.parse(input, outputter)
+                          case Right(saxContentHandler) =>
+                            val errorHandler = new CommandLineSAXErrorHandler()
+                            val parseResult = parseWithSAX(processor, input, saxContentHandler,
+                              errorHandler)
+                            parseResult
+                        }
                       })
                     }
 
@@ -1315,6 +1347,21 @@ object Main extends Logging {
     ret
   }
 
+  private def parseWithSAX(
+    processor: DFDL.DataProcessor,
+    data: InputSourceDataInputStream,
+    saxContentHandler: DaffodilOutputContentHandler,
+    errorHandler: CommandLineSAXErrorHandler): ParseResult = {
+    val saxXmlRdr = processor.newXMLReaderInstance
+    saxXmlRdr.setContentHandler(saxContentHandler)
+    saxXmlRdr.setErrorHandler(errorHandler)
+    saxXmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY, blobDir)
+    saxXmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX, blobSuffix)
+    saxXmlRdr.parse(data)
+    val pr = saxXmlRdr.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT).asInstanceOf[ParseResult]
+    pr
+  }
+
   def bugFound(e: Exception): Int = {
     System.err.println("""|
                           |!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/dsom/ElementBase.scala b/daffodil-core/src/main/scala/org/apache/daffodil/dsom/ElementBase.scala
index a2dc4d9..0e10d9f 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/dsom/ElementBase.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/dsom/ElementBase.scala
@@ -191,9 +191,6 @@ trait ElementBase
    */
   def isArrayWithAtLeastOneRequiredArrayElement: Boolean
 
-  final protected lazy val thisElementsNamespace: NS = this.namedQName.namespace
-  final protected lazy val thisElementsNamespacePrefix: String = this.namespaces.getPrefix(thisElementsNamespace.toString)
-
   private def nsBindingsToSet(nsb: NamespaceBinding): Set[(String, NS)] = {
     if (nsb == scala.xml.TopScope) Set()
     else {
@@ -207,7 +204,7 @@ trait ElementBase
     val childrenRequiredNSBindings =
       this.elementChildren.flatMap { _.thisElementsRequiredNamespaceBindings }.toSet
 
-    val myRequiredNSBinding = Set((thisElementsNamespacePrefix, thisElementsNamespace))
+    val myRequiredNSBinding = Set((namedQName.prefixOrNull, namedQName.namespace))
     val nilNSBinding = {
       if (!isNillable) Set()
       else {
diff --git a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/ElementBaseRuntime1Mixin.scala b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/ElementBaseRuntime1Mixin.scala
index d96379b..dc726d6 100644
--- a/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/ElementBaseRuntime1Mixin.scala
+++ b/daffodil-core/src/main/scala/org/apache/daffodil/runtime1/ElementBaseRuntime1Mixin.scala
@@ -173,7 +173,6 @@ trait ElementBaseRuntime1Mixin { self: ElementBase =>
       defaultBitOrder,
       optPrimType,
       targetNamespace,
-      thisElementsNamespace,
       optSimpleTypeRuntimeData,
       optComplexTypeModelGroupRuntimeData,
       minOccurs,
@@ -181,7 +180,6 @@ trait ElementBaseRuntime1Mixin { self: ElementBase =>
       Maybe.toMaybe(optionOccursCountKind),
       name,
       targetNamespacePrefix,
-      thisElementsNamespacePrefix,
       isNillable,
       isArray, // can have more than 1 occurrence
       isOptional, // can have exactly 0 or 1 occurrence
diff --git a/daffodil-core/src/test/scala/org/apache/daffodil/dsom/TestSimpleTypeUnions.scala b/daffodil-core/src/test/scala/org/apache/daffodil/dsom/TestSimpleTypeUnions.scala
index 98ae9bf..35c8c14 100644
--- a/daffodil-core/src/test/scala/org/apache/daffodil/dsom/TestSimpleTypeUnions.scala
+++ b/daffodil-core/src/test/scala/org/apache/daffodil/dsom/TestSimpleTypeUnions.scala
@@ -80,9 +80,9 @@ class TestSimpleTypeUnions {
     val Seq(st1, st2) = u.unionMemberTypes
     st1.asInstanceOf[GlobalSimpleTypeDef].globalQName.toQNameString
     val st1n = st1.diagnosticDebugName
-    assertEquals("ex:int1Type", st1n)
+    assertEquals("int1Type", st1n)
     val st2n = st2.diagnosticDebugName
-    assertEquals("ex:int2Type", st2n)
+    assertEquals("int2Type", st2n)
     val st1rd = st1.simpleTypeRuntimeData
     val st2rd = st2.simpleTypeRuntimeData
     val st1mini = st1rd.minInclusive
@@ -91,15 +91,15 @@ class TestSimpleTypeUnions {
     assertTrue(st2rd.unionMemberTypes.isEmpty)
     val st2mini = st2rd.minInclusive
     assertEquals(2, st2mini.get.intValue())
-    assertEquals("ex:int1Type", st1rd.diagnosticDebugName)
-    assertEquals("ex:int2Type", st2rd.diagnosticDebugName)
+    assertEquals("int1Type", st1rd.diagnosticDebugName)
+    assertEquals("int2Type", st2rd.diagnosticDebugName)
   }
 
   @Test def testUnionFirstUnionMemberOk: Unit = {
     val (result, actual) = TestUtils.testString(testSchema1, "1")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:int1Type", umstrd.diagnosticDebugName)
+    assertEquals("int1Type", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>1</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -109,7 +109,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema1, "2")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:int2Type", umstrd.diagnosticDebugName)
+    assertEquals("int2Type", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>2</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -128,13 +128,13 @@ class TestSimpleTypeUnions {
     assertTrue(d.isInstanceOf[ValidationError])
     val msg = d.getMessage()
     def die = { println(msg); fail() }
-    if (!msg.contains("ex:e1"))
+    if (!msg.contains("e1"))
       die
     if (!msg.contains("union members"))
       die
-    if (!msg.contains("ex:int1Type"))
+    if (!msg.contains("int1Type"))
       die
-    if (!msg.contains("ex:int2Type"))
+    if (!msg.contains("int2Type"))
       die
     if (!msg.contains("failed facet checks"))
       die
@@ -216,13 +216,13 @@ class TestSimpleTypeUnions {
     assertTrue(d.isInstanceOf[ValidationError])
     val msg = d.getMessage()
     def die = { println(msg); fail() }
-    if (!msg.contains("ex:e1"))
+    if (!msg.contains("e1"))
       die
     if (!msg.contains("union members"))
       die
-    if (!msg.contains("ex:int12or47Type"))
+    if (!msg.contains("int12or47Type"))
       die
-    if (!msg.contains("ex:negIntType"))
+    if (!msg.contains("negIntType"))
       die
     if (!msg.contains("failed facet checks"))
       die
@@ -232,7 +232,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema2, "1")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:int12Type", umstrd.diagnosticDebugName)
+    assertEquals("int12Type", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>1</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -242,7 +242,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema2, "2")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:int12Type", umstrd.diagnosticDebugName)
+    assertEquals("int12Type", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>2</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -252,7 +252,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema2, "-1")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:negIntType", umstrd.diagnosticDebugName) // anonymous simple type gets this name from base.
+    assertEquals("negIntType", umstrd.diagnosticDebugName) // anonymous simple type gets this name from base.
     assertTrue(i.valid.get)
     val expected = <e1>-1</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -309,7 +309,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema3, "foo3bar")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:foo3or4bar", umstrd.diagnosticDebugName)
+    assertEquals("foo3or4bar", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>foo3bar</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -319,7 +319,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema3, "foo1bar")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:foo1or2bar", umstrd.diagnosticDebugName)
+    assertEquals("foo1or2bar", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>foo1bar</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -329,7 +329,7 @@ class TestSimpleTypeUnions {
     val (result, actual) = TestUtils.testString(testSchema3, "foo2bar")
     val i = result.resultState.asInstanceOf[PState].infoset.asInstanceOf[DIDocument].contents(0).asInstanceOf[DISimple]
     val umstrd = i.unionMemberRuntimeData.get
-    assertEquals("ex:foo1or2bar", umstrd.diagnosticDebugName)
+    assertEquals("foo1or2bar", umstrd.diagnosticDebugName)
     assertTrue(i.valid.get)
     val expected = <e1>foo2bar</e1>
     TestUtils.assertEqualsXMLElements(expected, actual)
@@ -348,13 +348,13 @@ class TestSimpleTypeUnions {
     assertTrue(d.isInstanceOf[ValidationError])
     val msg = d.getMessage()
     def die = { println(msg); fail() }
-    if (!msg.contains("ex:e1"))
+    if (!msg.contains("e1"))
       die
     if (!msg.contains("union members"))
       die
-    if (!msg.contains("ex:foo1or2bar"))
+    if (!msg.contains("foo1or2bar"))
       die
-    if (!msg.contains("ex:foo3or4bar"))
+    if (!msg.contains("foo3or4bar"))
       die
     if (!msg.contains("failed facet checks"))
       die
@@ -377,7 +377,7 @@ class TestSimpleTypeUnions {
     assertTrue(d.isInstanceOf[ValidationError])
     val msg = d.getMessage()
     def die = { println(msg); fail() }
-    if (!msg.contains("ex:e1"))
+    if (!msg.contains("e1"))
       die
     if (!msg.contains("facet pattern"))
       die
diff --git a/daffodil-core/src/test/scala/org/apache/daffodil/util/TestUtils.scala b/daffodil-core/src/test/scala/org/apache/daffodil/util/TestUtils.scala
index c1d23c2..df9ade1 100644
--- a/daffodil-core/src/test/scala/org/apache/daffodil/util/TestUtils.scala
+++ b/daffodil-core/src/test/scala/org/apache/daffodil/util/TestUtils.scala
@@ -348,6 +348,7 @@ class Fakes private () {
     override def withTunables(tunables: Map[String,String]): DFDL.DataProcessor = this
     override def withValidationMode(mode: ValidationMode.Type): DFDL.DataProcessor = this
 
+    override def newXMLReaderInstance: DFDL.DaffodilXMLReader = null
   }
   lazy val fakeDP = new FakeDataProcessor
 
diff --git a/daffodil-japi/src/main/java/org/apache/daffodil/japi/package-info.java b/daffodil-japi/src/main/java/org/apache/daffodil/japi/package-info.java
index dea0152..efc540a 100644
--- a/daffodil-japi/src/main/java/org/apache/daffodil/japi/package-info.java
+++ b/daffodil-japi/src/main/java/org/apache/daffodil/japi/package-info.java
@@ -23,7 +23,7 @@
  * <h3>Overview</h3>
  *
  * The {@link org.apache.daffodil.japi.Daffodil} object is a factory object to create a {@link org.apache.daffodil.japi.Compiler}. The
- * {@link org.apache.daffodil.japi.Compiler} provides a method to compils a provided DFDL schema into a
+ * {@link org.apache.daffodil.japi.Compiler} provides a method to compile a provided DFDL schema into a
  * {@link org.apache.daffodil.japi.ProcessorFactory}, which creates a {@link org.apache.daffodil.japi.DataProcessor}:
  *
  * <pre>
@@ -36,10 +36,69 @@
  * The {@link org.apache.daffodil.japi.DataProcessor} provides the necessary functions to parse and unparse
  * data, returning a {@link org.apache.daffodil.japi.ParseResult} or {@link org.apache.daffodil.japi.UnparseResult}, respectively. These
  * contain information about the parse/unparse, such as whether or not the
- * processing succeeded any diagnostic information.
+ * processing succeeded with any diagnostic information.
+ *
+ * The {@link org.apache.daffodil.japi.DataProcessor} also provides a function to create a
+ * {@link org.apache.daffodil.japi.DaffodilXMLReader} that can be used to perform parsing via the
+ * SAX API.
+ *
+ * <pre>
+ * {@code
+ * DaffodilXMLReader xmlRdr = dp.newXMLReaderInstance();
+ * }</pre>
+ *
+ * The {@link org.apache.daffodil.japi.DaffodilXMLReader} has several methods that allow one to
+ * set properties and handlers (such as ContentHandlers or ErrorHandlers) for the reader. One can
+ * use any contentHandler/errorHandler as long as they extend the
+ * {@link org.xml.sax.ContentHandler} and {@link org.xml.sax.ErrorHandler} interfaces
+ * respectively. One can also set properties for the {@link org.apache.daffodil.japi.DaffodilXMLReader}
+ * using {@link org.apache.daffodil.japi.DaffodilXMLReader#setProperty(java.lang.String, java.lang.Object)}.
+ *
+ * The following properties can be set as follows:
+ * <pre>
+ * {@code
+ * xmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY(), "/tmp/");
+ * xmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX(), "daffodil-sax-");
+ * xmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX(), ".bin");
+ * }
+ * </pre>
+ *
+ * The variables above start with "urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:sax:" and end
+ * with BlobDirectory, BlobPrefix and BlobSuffix respectively.
+ *
+ * The properites can be retrieved using the same variables with
+ * {@link org.apache.daffodil.japi.DaffodilXMLReader#getProperty(java.lang.String)}
+ *
+ * The following handlers can be set as follows:
+ * <pre>
+ * {@code
+ * xmlRdr.setContentHandler(contentHandler);
+ * xmlRdr.setErrorHandler(errorHandler);
+ * xmlRdr.setDTDHandler(dtdHandler);
+ * xmlRdr.setEntityResolver(entityResolver);
+ * }
+ * </pre>
+ *
+ * The handlers above must implement the following interfaces respectively:
+ * <pre>
+ * {@code
+ * org.xml.sax.ContentHandler
+ * org.xml.sax.ErrorHandler
+ * org.xml.sax.DTDHandler
+ * org.xml.sax.EntityResolver
+ * }
+ * </pre>
+ *
+ * The {@link org.apache.daffodil.japi.ParseResult} can be found as a property within the
+ * {@link org.apache.daffodil.japi.DaffodilXMLReader} using
+ * {@link org.apache.daffodil.japi.DaffodilXMLReader#getProperty(java.lang.String)} together
+ * with the following uri: "urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:sax:ParseResult" or
+ * XMLUtils.DAFFODIL_SAX_URN_PARSERESULT().
  *
  * <h4>Parse</h4>
  *
+ * <h5>Dataprocessor Parse</h5>
+ *
  * The {@link org.apache.daffodil.japi.DataProcessor#parse(org.apache.daffodil.japi.io.InputSourceDataInputStream, org.apache.daffodil.japi.infoset.InfosetOutputter)} method accepts input data to parse in the form
  * of a {@link org.apache.daffodil.japi.io.InputSourceDataInputStream} and an {@link org.apache.daffodil.japi.infoset.InfosetOutputter}
  * to determine the output representation of the infoset (e.g. Scala XML Nodes,
@@ -85,6 +144,66 @@
  * }
  * }</pre>
  *
+ * <h5>SAX Parse</h5>
+ * The {@link org.apache.daffodil.japi.DaffodilXMLReader#parse(
+ * org.apache.daffodil.japi.io.InputSourceDataInputStream)} method accepts input data to parse in
+ * the form of a {@link org.apache.daffodil.japi.io.InputSourceDataInputStream}. The output
+ * representation of the infoset, as well as how parse errors are handled, are dependent on the
+ * content handler and the error handler provided to the {@link org.apache.daffodil.japi.DaffodilXMLReader}. For example the
+ * {@link org.jdom2.input.sax.SAXHandler} provides a JDOM representation, whereas other Content
+ * Handlers may output directly to an {@link java.io.OutputStream} or {@link java.io.Writer}.
+ *
+ * <pre>
+ * {@code
+ * SAXHandler contentHandler = new SAXHandler();
+ * xmlRdr.setContentHandler(contentHandler);
+ * InputSourceDataInputStream is = new InputSourceDataInputStream(data);
+ * xmlReader.parse(is);
+ * ParseResult pr = (ParseResult) xmlRdr.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT());
+ * Document doc = saxHandler.getDocument();
+ * }</pre>
+ *
+ * The The {@link org.apache.daffodil.japi.DaffodilXMLReader#parse(
+ * org.apache.daffodil.japi.io.InputSourceDataInputStream)} method is not thread-safe and may
+ * only be called again/reused once a parse operation is completed. This can be done multiple
+ * times without the need to create new DaffodilXMLReaders, ContentHandlers or ErrorHandlers. It
+ * might be necessary to reset whatever ContentHandler is used (or allocate a new one). A
+ * thread-safe implementation would require unique instances of the DaffodilXMLReader and its
+ * components. For example:
+ *
+ * <pre>
+ * {@code
+ * SAXHandler contentHandler = new SAXHandler();
+ * xmlRdr.setContentHandler(contentHandler);
+ * for (File f : inputFiles) {
+ *   contentHandler.reset();
+ *   InputSourceDataInputStream is = new InputSourceDataInputStream(new FileInputStream(f));
+ *   xmlReader.parse(is);
+ *   ParseResult pr = (ParseResult) xmlRdr.getProperty("urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:sax:ParseResult");
+ *   Document doc = saxHandler.getDocument();
+ * }
+ * }
+ * </pre>
+ *
+ * One can repeat calls to parse() using the same InputSourceDataInputStream to continue parsing
+ * where the previous parse ended. For example:
+ *
+ * <pre>
+ * {@code
+ * InputSourceDataInputStream is = new InputSourceDataInputStream(dataStream);
+ * SAXHandler contentHandler = new SAXHandler();
+ * xmlRdr.setContentHandler(contentHandler);
+ * Boolean keepParsing = true;
+ * while (keepParsing) {
+ *   contentHandler.reset();
+ *   xmlRdr.parse(is);
+ *   val pr = xmlRdr.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT());
+ *   ...
+ *   keepParsing = !pr.location().isAtEnd() && !pr.isError();
+ * }
+ * }
+ * </pre>
+ *
  * <h4>Unparse</h4>
  *
  * The same {@link org.apache.daffodil.japi.DataProcessor} used for parse can be used to unparse an infoset
@@ -141,6 +260,22 @@
  * ParseResult pr = dp.parse(data);
  * }</pre>
  *
+ * And use like below:
+ * <pre>
+ * {@code
+ * ParseResult pr = dp.parse(data);
+ * }</pre>
+ *
+ * or
+ *
+ * <pre>
+ * {@code
+ * DaffodilXMLReader xmlRdr = dp.newXMLReaderInstance();
+ * ... // setting appropriate handlers
+ * xmlReader.parse(data);
+ * ParseResult pr = xmlRdr.getProperty("...ParseResult");
+ * }</pre>
+ *
  */
 
 package org.apache.daffodil.japi;
diff --git a/daffodil-japi/src/main/scala/org/apache/daffodil/japi/Daffodil.scala b/daffodil-japi/src/main/scala/org/apache/daffodil/japi/Daffodil.scala
index 3feca34..68a1815 100644
--- a/daffodil-japi/src/main/scala/org/apache/daffodil/japi/Daffodil.scala
+++ b/daffodil-japi/src/main/scala/org/apache/daffodil/japi/Daffodil.scala
@@ -38,6 +38,7 @@ import org.apache.daffodil.api.{ LocationInSchemaFile => SLocationInSchemaFile }
 import org.apache.daffodil.api.{ WithDiagnostics => SWithDiagnostics }
 import org.apache.daffodil.compiler.{ ProcessorFactory => SProcessorFactory }
 import org.apache.daffodil.processors.{ DataProcessor => SDataProcessor }
+import org.apache.daffodil.processors.{ DaffodilXMLReader => SDaffodilXMLReader }
 import org.apache.daffodil.processors.{ ParseResult => SParseResult }
 import org.apache.daffodil.processors.{ UnparseResult => SUnparseResult }
 import org.apache.daffodil.util.{ ConsoleWriter => SConsoleWriter }
@@ -657,6 +658,12 @@ class DataProcessor private[japi] (private var dp: SDataProcessor)
   def save(output: WritableByteChannel): Unit = dp.save(output)
 
   /**
+   *  Obtain a new [[DaffodilXMLReader]] from the current [[DataProcessor]].
+   */
+  def newXMLReaderInstance: DaffodilXMLReader =
+    new DaffodilXMLReader(xmlrdr = dp.newXMLReaderInstance.asInstanceOf[SDaffodilXMLReader])
+
+  /**
    * Parse input data with a specified length
    *
    * @param input data to be parsed
@@ -827,3 +834,122 @@ class InvalidParserException private[japi] (cause: org.apache.daffodil.compiler.
  * This exception will be thrown as a result of an invalid usage of the Daffodil API
  */
 class InvalidUsageException private[japi] (cause: org.apache.daffodil.processors.InvalidUsageException) extends Exception(cause.getMessage(), cause.getCause())
+
+/**
+ * SAX method of parsing schema and getting the DFDL Infoset via some
+ * org.xml.sax.ContentHandler, based on the org.xml.sax.XMLReader interface
+ */
+class DaffodilXMLReader private[japi] (xmlrdr: SDaffodilXMLReader) extends org.xml.sax.XMLReader {
+  /**
+   * Get the value of the feature flag
+   * @param name feature flag whose value is to be retrieved
+   * @return value of the feature flag
+   */
+  override def getFeature(name: String): Boolean = xmlrdr.getFeature(name)
+
+  /**
+   * Set the value of the feature flag
+   * @param name feature flag to be set
+   * @param value value to be assigned to feature flag
+   */
+  override def setFeature(name: String, value: Boolean): Unit = xmlrdr.setFeature(name, value)
+
+  /**
+   * Get the value of the property
+   * @param name property whose value is to be retrieved
+   * @return value of the property
+   */
+  override def getProperty(name: String): AnyRef = xmlrdr.getProperty(name)
+
+  /**
+   * Set the value of the property
+   * @param name property whose value is to be set
+   * @param value value to be assigned to the property
+   */
+  override def setProperty(name: String, value: AnyRef): Unit = xmlrdr.setProperty(name, value)
+
+  /**
+   * Register an entity resolver
+   * @param resolver entity resolver to be registered
+   */
+  override def setEntityResolver(resolver: org.xml.sax.EntityResolver): Unit =
+    xmlrdr.setEntityResolver(resolver)
+
+  /**
+   * Return the registered entity resolver
+   * @return registered entity resolver or null
+   */
+  override def getEntityResolver: org.xml.sax.EntityResolver = xmlrdr.getEntityResolver
+
+  /**
+   * Register a DTD Handler
+   * @param handler DTD Handler to be registered
+   */
+  override def setDTDHandler(handler: org.xml.sax.DTDHandler): Unit = xmlrdr.setDTDHandler(handler)
+
+  /**
+   * Retrieve registered DTD Handler
+   * @return registered DTD Handler or null
+   */
+  override def getDTDHandler: org.xml.sax.DTDHandler = xmlrdr.getDTDHandler
+
+  /**
+   * Register a content handler
+   * @param handler content handler to be registered
+   */
+  override def setContentHandler(handler: org.xml.sax.ContentHandler): Unit =
+    xmlrdr.setContentHandler(handler)
+
+  /**
+   * Retrieve registered content handler
+   * @return registered content handler or null
+   */
+  override def getContentHandler: org.xml.sax.ContentHandler = xmlrdr.getContentHandler
+
+  /**
+   * Register an error handler
+   * @param handler error handler to be registered
+   */
+  override def setErrorHandler(handler: org.xml.sax.ErrorHandler): Unit =
+    xmlrdr.setErrorHandler(handler)
+
+  /**
+   * Retrieve registered error handler
+   * @return registered error handler or null
+   */
+  override def getErrorHandler: org.xml.sax.ErrorHandler = xmlrdr.getErrorHandler
+
+  /**
+   * Parse input data from an InputSource. Infoset can be retrieved via the registered
+   * contentHandler and diagnostics via the registered errorHandler
+   * @param input data to be parsed
+   */
+  override def parse(input: org.xml.sax.InputSource): Unit = xmlrdr.parse(input)
+
+  /**
+   * Parse data from a system identifier/URI. This method is not supported by the API.
+   * @param systemId URI for data to be parsed
+   */
+  override def parse(systemId: String): Unit = xmlrdr.parse(systemId)
+
+  /**
+   * Parse input data from an InputSourceDataInputStream. Infoset can retrieved via the registered
+   * contentHandler and diagnostics via the registered errorHandler
+   * @param isdis data to be parsed
+   */
+  def parse(isdis: InputSourceDataInputStream): Unit = xmlrdr.parse(isdis.dis)
+
+  /**
+   * Parse input data from an InputStream. Infoset can retrieved via the registered contentHandler
+   * and diagnostics via the registered errorHandler
+   * @param stream data to be parsed
+   */
+  def parse(stream: java.io.InputStream): Unit = xmlrdr.parse(stream)
+
+  /**
+   * Parse input data from an array of bytes. Infoset can retrieved via the registered
+   * contentHandler and diagnostics via the registered errorHandler
+   * @param arr data to be parsed
+   */
+  def parse(arr: Array[Byte]): Unit = xmlrdr.parse(arr)
+}
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 284c1e9..c0116b0 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
@@ -24,17 +24,19 @@ import static org.junit.Assert.fail;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
 import java.io.File;
 import java.io.IOException;
-import java.nio.channels.Channel;
 import java.nio.channels.Channels;
 import java.nio.channels.ReadableByteChannel;
 import java.nio.channels.WritableByteChannel;
+import java.nio.file.Paths;
+import java.util.ArrayList;
 import java.util.List;
 
+import org.apache.daffodil.infoset.DaffodilOutputContentHandler;
 import org.apache.daffodil.japi.*;
+import org.apache.daffodil.japi.infoset.XMLTextInfosetOutputter;
+import org.apache.daffodil.xml.XMLUtils;
 import org.jdom2.output.Format;
 import org.junit.Test;
 
@@ -955,4 +957,88 @@ public class TestJavaAPI {
       }
     }
 
+    @Test
+    public void testJavaAPI20() throws IOException, ClassNotFoundException {
+        // Test SAX parsing
+        org.apache.daffodil.japi.Compiler c = Daffodil.compiler();
+        java.io.File schemaFile = getResource("/test/japi/mySchema1.dfdl.xsd");
+        ProcessorFactory pf = c.compileFile(schemaFile);
+        DataProcessor dp = pf.onPath("/");
+        dp = reserializeDataProcessor(dp);
+        DaffodilXMLReader xri = dp.newXMLReaderInstance();
+
+        java.io.File file = getResource("/test/japi/myData.dat");
+        java.io.FileInputStream fisDP = new java.io.FileInputStream(file);
+        java.io.FileInputStream fisSAX = new java.io.FileInputStream(file);
+        InputSourceDataInputStream disDP = new InputSourceDataInputStream(fisDP);
+        InputSourceDataInputStream disSAX = new InputSourceDataInputStream(fisSAX);
+        ByteArrayOutputStream xmlBos = new ByteArrayOutputStream();
+        XMLTextInfosetOutputter outputter = new XMLTextInfosetOutputter(xmlBos, true);
+        ParseResult res = dp.parse(disDP, outputter);
+        String infosetDPString = xmlBos.toString();
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        DaffodilOutputContentHandler contentHandler = new DaffodilOutputContentHandler(bos, true);
+        SAXErrorHandlerForJAPITest errorHandler = new SAXErrorHandlerForJAPITest();
+        xri.setContentHandler(contentHandler);
+        xri.setErrorHandler(errorHandler);
+        xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY(),
+                Paths.get(System.getProperty("java.io.tmpdir")));
+        xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX(),
+                "daffodil-sapi-");
+        xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX(),
+                ".sax.blob");
+        xri.parse(disSAX);
+        org.apache.daffodil.processors.ParseResult resSAX =
+                (org.apache.daffodil.processors.ParseResult) xri.getProperty(
+                        XMLUtils.DAFFODIL_SAX_URN_PARSERESULT());
+        boolean err = errorHandler.isError();
+        ArrayList<Diagnostic> diags = errorHandler.getDiagnostics();
+        String infosetSAXString = bos.toString();
+
+        assertFalse(err);
+        assertTrue(resSAX.resultState().currentLocation().isAtEnd());
+        assertTrue(diags.isEmpty());
+        assertEquals(infosetDPString, infosetSAXString);
+    }
+
+
+    @Test
+    public void testJavaAPI21() throws IOException, ClassNotFoundException {
+        // Test SAX parsing with errors
+        org.apache.daffodil.japi.Compiler c = Daffodil.compiler();
+        java.io.File schemaFile = getResource("/test/japi/mySchema1.dfdl.xsd");
+        ProcessorFactory pf = c.compileFile(schemaFile);
+        DataProcessor dp = pf.onPath("/");
+        dp = reserializeDataProcessor(dp);
+        DaffodilXMLReader xri = dp.newXMLReaderInstance();
+
+        java.io.File file = getResource("/test/japi/myDataBroken.dat");
+        java.io.FileInputStream fis = new java.io.FileInputStream(file);
+        InputSourceDataInputStream dis = new InputSourceDataInputStream(fis);
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        DaffodilOutputContentHandler contentHandler = new DaffodilOutputContentHandler(bos, false);
+        SAXErrorHandlerForJAPITest errorHandler = new SAXErrorHandlerForJAPITest();
+        xri.setContentHandler(contentHandler);
+        xri.setErrorHandler(errorHandler);
+        xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY(),
+                Paths.get(System.getProperty("java.io.tmpdir")));
+        xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX(), "daffodil-sapi-");
+        xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX(), ".sax.blob");
+        xri.parse(dis);
+        boolean err = errorHandler.isError();
+        ArrayList<Diagnostic> diags = errorHandler.getDiagnostics();
+
+        assertTrue(err);
+        assertEquals(1, diags.size());
+        Diagnostic d = diags.get(0);
+        assertTrue(d.getMessage().contains("int"));
+        assertTrue(d.getMessage().contains("Not an int"));
+        assertTrue(d.getDataLocations().toString().contains("10"));
+        java.util.List<LocationInSchemaFile> locs = d.getLocationsInSchemaFiles();
+        assertEquals(1, locs.size());
+        LocationInSchemaFile loc = locs.get(0);
+        assertTrue(loc.toString().contains("mySchema1.dfdl.xsd"));
+    }
 }
diff --git a/daffodil-japi/src/test/java/org/apache/daffodil/japi/SAXErrorHandlerForJAPITest.java b/daffodil-japi/src/test/java/org/apache/daffodil/japi/SAXErrorHandlerForJAPITest.java
new file mode 100644
index 0000000..f646fdb
--- /dev/null
+++ b/daffodil-japi/src/test/java/org/apache/daffodil/japi/SAXErrorHandlerForJAPITest.java
@@ -0,0 +1,60 @@
+/*
+ * 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;
+
+import org.apache.daffodil.api.Diagnostic;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import scala.collection.Seq;
+
+import java.util.ArrayList;
+
+public class SAXErrorHandlerForJAPITest implements ErrorHandler {
+    private ArrayList<org.apache.daffodil.japi.Diagnostic> _diagnostics = new ArrayList<>();
+    private Boolean _isError = false;
+
+    @Override
+    public void warning(SAXParseException exception) throws SAXException {
+        _isError = false;
+        org.apache.daffodil.japi.Diagnostic embeddedDiagnostic =
+                new org.apache.daffodil.japi.Diagnostic((Diagnostic) exception.getCause());
+        _diagnostics.add(embeddedDiagnostic);
+    }
+
+    @Override
+    public void error(SAXParseException exception) throws SAXException {
+        _isError = true;
+        org.apache.daffodil.japi.Diagnostic embeddedDiagnostic =
+                new org.apache.daffodil.japi.Diagnostic((Diagnostic) exception.getCause());
+        _diagnostics.add(embeddedDiagnostic);
+    }
+
+    @Override
+    public void fatalError(SAXParseException exception) throws SAXException {
+        error(exception);
+    }
+
+    public ArrayList<org.apache.daffodil.japi.Diagnostic> getDiagnostics() {
+        return _diagnostics;
+    }
+
+    public boolean isError() {
+        return _isError;
+    }
+}
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/util/Validator.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/util/Validator.scala
index f0cb8b9..1e5684a 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/util/Validator.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/util/Validator.scala
@@ -17,12 +17,14 @@
 
 package org.apache.daffodil.util
 
-import javax.xml.transform.stream.StreamSource
-import javax.xml.XMLConstants
-import scala.xml.parsing.NoBindingFactoryAdapter
 import java.net.URI
-import org.apache.daffodil.xml.DFDLCatalogResolver
+
 import scala.collection.mutable
+import scala.xml.parsing.NoBindingFactoryAdapter
+
+import javax.xml.XMLConstants
+import javax.xml.transform.stream.StreamSource
+import org.apache.daffodil.xml.DFDLCatalogResolver
 import org.xml.sax.ErrorHandler
 
 /**
@@ -45,8 +47,14 @@ object Validator extends NoBindingFactoryAdapter {
     val validator = {
       val optCachedValidator = cache.get(schemaFileNames)
       optCachedValidator match {
-        case Some(validator) => {
-          validator.reset()
+        case Some(cachedValidator) => {
+          cachedValidator.reset()
+          // reset takes it back to the original state at the point we call
+          // newValidator. So we need to re-set the features, resolvers,
+          // and handlers
+          val resolver = DFDLCatalogResolver.get
+          val validator : javax.xml.validation.Validator =
+            initializeValidator(cachedValidator, errHandler, resolver)
           validator
         }
         case None => {
@@ -65,18 +73,7 @@ object Validator extends NoBindingFactoryAdapter {
           val resolver = DFDLCatalogResolver.get
           factory.setResourceResolver(resolver)
           val schema = factory.newSchema(schemaSources.toArray)
-          val validator = schema.newValidator()
-          validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
-          //
-          validator.setFeature("http://xml.org/sax/features/validation", true)
-
-          // If you enable the feature below, it seems to do no validation at all. Just passes.
-          //          validator.setFeature("http://apache.org/xml/features/validation/dynamic", true)
-
-          validator.setFeature("http://apache.org/xml/features/validation/schema", true)
-          validator.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true)
-          validator.setErrorHandler(errHandler)
-          validator.setResourceResolver(resolver)
+          val validator = initializeValidator(schema.newValidator(), errHandler, resolver)
           cache.put(schemaFileNames, validator)
           validator
         }
@@ -85,5 +82,21 @@ object Validator extends NoBindingFactoryAdapter {
     val documentSource = new StreamSource(document)
     validator.validate(documentSource)
   }
+
+  def initializeValidator(validator: javax.xml.validation.Validator, errHandler: ErrorHandler, resolver: DFDLCatalogResolver):
+    javax.xml.validation.Validator = {
+    validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true)
+    //
+    validator.setFeature("http://xml.org/sax/features/validation", true)
+
+    // If you enable the feature below, it seems to do no validation at all. Just passes.
+    //          validator.setFeature("http://apache.org/xml/features/validation/dynamic", true)
+
+    validator.setFeature("http://apache.org/xml/features/validation/schema", true)
+    validator.setFeature("http://apache.org/xml/features/validation/schema-full-checking", true)
+    validator.setErrorHandler(errHandler)
+    validator.setResourceResolver(resolver)
+    validator
+  }
 }
 
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/xml/QNameBase.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/xml/QNameBase.scala
index b7d4f09..4d35706 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/xml/QNameBase.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/xml/QNameBase.scala
@@ -183,27 +183,9 @@ object QName {
       else if (targetNamespace == NoNamespace) None
       else if (targetNamespace == UnspecifiedNamespace) None
       else {
-        val prefixes = NS.allPrefixes(targetNamespace, scope)
-        prefixes.length match {
-          case 0 => None
-          case 1 => Option(prefixes.head)
-          case _ => {
-            //
-            // suppose we have xmlns="..." xmlns:ex="..." xmlns:tns="..."
-            //
-            // We want to prefer ex as the prefix in this case.
-            // So we take the shortest prefix that isn't empty string.
-            //
-            val notNullPrefixes = prefixes.filter(_ ne null) // remove null prefix. There must be a non-empty one.
-            notNullPrefixes.foreach { p => Assert.invariant(p.length > 0) }
-            val pairs = notNullPrefixes.map { _.length } zip notNullPrefixes
-            val first = pairs.head // there has to be 1 at least. But there might be only 1
-            val minLengthPair = pairs.foldLeft(first) { case (x @ (xlen, _), y @ (ylen, _)) => if (xlen <= ylen) x else y }
-            val (n, shortest) = minLengthPair
-            Assert.invariant(n > 0)
-            Some(shortest)
-          }
-        }
+        val prefix = scope.getPrefix(targetNamespace)
+        if (prefix eq null) None
+        else Some(prefix)
       }
 
   def createGlobal(name: String, targetNamespace: NS, scope: scala.xml.NamespaceBinding) = {
@@ -243,6 +225,7 @@ trait QNameBase extends Serializable {
    * or by omitting one, are very common.
    */
   def prefix: Option[String]
+  def prefixOrNull: String = prefix.orNull
   def local: String
   def namespace: NS // No namespace is represented by the NoNamespace object.
 
diff --git a/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala b/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala
index 698d1cb..ac51669 100644
--- a/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala
+++ b/daffodil-lib/src/main/scala/org/apache/daffodil/xml/XMLUtils.scala
@@ -28,13 +28,12 @@ import scala.xml.NamespaceBinding
 import scala.xml._
 
 import org.apache.commons.io.IOUtils
-
-import org.apache.daffodil.exceptions._
-import org.apache.daffodil.util.Misc
 import org.apache.daffodil.calendar.DFDLDateConversion
 import org.apache.daffodil.calendar.DFDLDateTimeConversion
 import org.apache.daffodil.calendar.DFDLTimeConversion
+import org.apache.daffodil.exceptions._
 import org.apache.daffodil.schema.annotation.props.LookupLocation
+import org.apache.daffodil.util.Misc
 
 /**
  * Utilities for handling XML
@@ -410,15 +409,22 @@ object XMLUtils {
   val EXT_PREFIX_NCSA = "daf"
   val EXT_NS_NCSA = NS(DAFFODIL_EXTENSION_NAMESPACE_NCSA.uri)
 
-  private val DAFFODIL_EXTENSIONS_NAMESPACE_ROOT_APACHE = "urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018"
-  private val DAFFODIL_EXTENSION_NAMESPACE_APACHE = NS(DAFFODIL_EXTENSIONS_NAMESPACE_ROOT_APACHE + ":ext")
+  private val DAFFODIL_NAMESPACE_ROOT_APACHE = "urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018"
+  private val DAFFODIL_EXTENSION_NAMESPACE_APACHE = NS(DAFFODIL_NAMESPACE_ROOT_APACHE + ":ext")
   val EXT_PREFIX_APACHE = "daf"
   val EXT_NS_APACHE = NS(DAFFODIL_EXTENSION_NAMESPACE_APACHE.uri)
 
-  private val DAFFODIL_INTERNAL_NAMESPACE = NS(DAFFODIL_EXTENSIONS_NAMESPACE_ROOT_APACHE + ":int")
+  private val DAFFODIL_INTERNAL_NAMESPACE = NS(DAFFODIL_NAMESPACE_ROOT_APACHE + ":int")
   val INT_PREFIX = "dafint"
   val INT_NS = NS(DAFFODIL_INTERNAL_NAMESPACE.uri)
 
+  val DAFFODIL_SAX_URN_ROOT: String = DAFFODIL_NAMESPACE_ROOT_APACHE + ":sax"
+  val DAFFODIL_SAX_URN_PARSERESULT: String = DAFFODIL_SAX_URN_ROOT + ":ParseResult"
+  val DAFFODIL_SAX_URN_BLOBDIRECTORY: String = DAFFODIL_SAX_URN_ROOT + ":BlobDirectory"
+  val DAFFODIL_SAX_URN_BLOBPREFIX: String = DAFFODIL_SAX_URN_ROOT + ":BlobPrefix"
+  val DAFFODIL_SAX_URN_BLOBSUFFIX: String = DAFFODIL_SAX_URN_ROOT + ":BlobSuffix"
+
+
   val FILE_ATTRIBUTE_NAME = "file"
   val LINE_ATTRIBUTE_NAME = "line"
   val COLUMN_ATTRIBUTE_NAME = "col"
@@ -798,10 +804,20 @@ object XMLUtils {
 
   class XMLDifferenceException(message: String) extends Exception(message)
 
-  def compareAndReport(expected: Node, actual: Node, ignoreProcInstr: Boolean = true) = {
+  def compareAndReport(
+    expected: Node,
+    actual: Node,
+    ignoreProcInstr: Boolean = true,
+    checkPrefixes: Boolean = false,
+    checkNamespaces: Boolean = false): Unit = {
     val expectedMinimized = prepareForDiffComparison(expected)
     val actualMinimized = prepareForDiffComparison(actual)
-    val diffs = XMLUtils.computeDiff(expectedMinimized, actualMinimized, ignoreProcInstr)
+    val diffs = XMLUtils.computeDiff(
+      expectedMinimized,
+      actualMinimized,
+      ignoreProcInstr,
+      checkPrefixes,
+      checkNamespaces)
     if (diffs.length > 0) {
       throw new XMLDifferenceException("""
 Comparison failed.
@@ -821,8 +837,21 @@ Differences were (path, expected, actual):
    * computes a precise difference list which is a sequence of triples.
    * Each triple is the path (an x-path-like string), followed by expected, and actual values.
    */
-  def computeDiff(a: Node, b: Node, ignoreProcInstr: Boolean = true) = {
-    computeDiffOne(a, b, None, Nil, ignoreProcInstr, None)
+  def computeDiff(
+    a: Node,
+    b: Node,
+    ignoreProcInstr: Boolean = true,
+    checkPrefixes: Boolean = false,
+    checkNamespaces: Boolean = false) = {
+    computeDiffOne(
+      a,
+      b,
+      None,
+      Nil,
+      ignoreProcInstr,
+      checkPrefixes,
+      checkNamespaces,
+      None)
   }
 
   def childArrayCounters(e: Elem) = {
@@ -841,21 +870,32 @@ Differences were (path, expected, actual):
     maybeIndex: Option[Int],
     parentPathSteps: Seq[String],
     ignoreProcInstr: Boolean,
+    checkPrefixes: Boolean,
+    checkNamespaces: Boolean,
     maybeType: Option[String]): Seq[(String, String, String)] = {
     lazy val zPath = parentPathSteps.reverse.mkString("/")
     (an, bn) match {
       case (a: Elem, b: Elem) => {
-        val Elem(_, labelA, attribsA, _, childrenA @ _*) = a
-        val Elem(_, labelB, attribsB, _, childrenB @ _*) = b
+        val Elem(prefixA, labelA, attribsA, nsbA, childrenA @ _*) = a
+        val Elem(prefixB, labelB, attribsB, nsbB, childrenB @ _*) = b
         val typeA: Option[String] = a.attribute(XSI_NAMESPACE.toString, "type").map(_.head.text)
         val typeB: Option[String] = b.attribute(XSI_NAMESPACE.toString, "type").map(_.head.text)
         val maybeType: Option[String] = Option(typeA.getOrElse(typeB.getOrElse(null)))
         val nilledA = a.attribute(XSI_NAMESPACE.toString, "nil")
         val nilledB = b.attribute(XSI_NAMESPACE.toString, "nil")
+        // we sort here, since sameElements is not order independent
+        val nsbACompare = nsbA.toString().trim.split(" ").sorted
+        val nsbBCompare = nsbB.toString().trim.split(" ").sorted
 
         if (labelA != labelB) {
           // different label
           List((zPath, labelA, labelB))
+        } else if (checkPrefixes && prefixA != prefixB) {
+          // different prefix
+          List((zPath, prefixA, prefixB))
+        } else if (checkNamespaces && !nsbACompare.sameElements(nsbBCompare)) {
+          // different namespace bindings
+          List((zPath, nsbA.toString(), nsbB.toString()))
         } else if (nilledA != nilledB) {
           // different xsi:nil
           List((zPath + "/" + labelA + "@xsi:nil",
@@ -893,7 +933,15 @@ Differences were (path, expected, actual):
               countMap(ca.label) += 1
               count + 1
             }
-            computeDiffOne(ca, cb, maybeChildIndex, thisPathStep, ignoreProcInstr, maybeType)
+            computeDiffOne(
+              ca,
+              cb,
+              maybeChildIndex,
+              thisPathStep,
+              ignoreProcInstr,
+              checkPrefixes,
+              checkNamespaces,
+              maybeType)
           }
 
           // if childrenA and childrenB have different length, zip will drop an
@@ -1009,7 +1057,9 @@ Differences were (path, expected, actual):
     dataB: String,
     maybeType: Option[String]): Seq[(String, String, String)] = {
 
-    if (maybeType.isDefined && maybeType.get == "xs:anyURI") computeBlobDiff(zPath, dataA, dataB)
+    val hasBlobType = maybeType.isDefined && maybeType.get == "xs:anyURI"
+    val dataLooksLikeBlobURI = Seq(dataA, dataB).forall(_.startsWith("file://"))
+    if (hasBlobType || dataLooksLikeBlobURI) computeBlobDiff(zPath, dataA, dataB)
     else if (textIsSame(dataA, dataB, maybeType)) Nil
     else {
       // There must be some difference, so let's find just the first index of
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/api/DFDLParserUnparser.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/api/DFDLParserUnparser.scala
index 39f1371..d63e6a6 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/api/DFDLParserUnparser.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/api/DFDLParserUnparser.scala
@@ -20,6 +20,7 @@ package org.apache.daffodil.api
 import org.apache.daffodil.processors.ProcessorResult
 import org.apache.daffodil.processors.Success
 import java.io.File
+
 import org.apache.daffodil.processors.VariableMap
 import org.apache.daffodil.externalvars.Binding
 import org.apache.daffodil.infoset.InfosetInputter
@@ -179,7 +180,10 @@ object DFDL {
     @deprecated("Use withTunables.", "2.6.0")
     def setTunables(tunables: Map[String,String]): Unit
 
-
+    /**
+     * Creates a new instance of XMLReader for SAX Parsing/Unparsing
+     */
+    def newXMLReaderInstance: DaffodilXMLReader
 
     /**
      * Unparses (that is, serializes) data to the output, returns an object which contains any diagnostics.
@@ -192,6 +196,12 @@ object DFDL {
     def parse(input: InputSourceDataInputStream, output: InfosetOutputter): ParseResult
   }
 
+  trait DaffodilXMLReader extends org.xml.sax.XMLReader {
+    def parse(is: java.io.InputStream): Unit
+    def parse(in: InputSourceDataInputStream): Unit
+    def parse(ab: Array[Byte]): Unit
+  }
+
   trait ParseResult extends Result with WithDiagnostics {
     def resultState: State
   }
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JDOMInfosetOutputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JDOMInfosetOutputter.scala
index 0fdfc34..18c03de 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JDOMInfosetOutputter.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JDOMInfosetOutputter.scala
@@ -99,11 +99,11 @@ class JDOMInfosetOutputter extends InfosetOutputter
 
   private def createElement(diElement: DIElement): org.jdom2.Element = {
     val elem: org.jdom2.Element =
-      if(diElement.erd.thisElementsNamespace.isNoNamespace)
+      if(diElement.erd.namedQName.namespace.isNoNamespace)
         new org.jdom2.Element(diElement.erd.name)
       else
-        new org.jdom2.Element(diElement.erd.name, diElement.erd.thisElementsNamespacePrefix,
-          diElement.erd.thisElementsNamespace)
+        new org.jdom2.Element(diElement.erd.name, diElement.erd.namedQName.prefixOrNull,
+          diElement.erd.namedQName.namespace)
 
     if (isNilled(diElement)) {
       elem.setAttribute("nil", "true", xsiNS)
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JsonInfosetOutputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JsonInfosetOutputter.scala
index 8e39225..c521e37 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JsonInfosetOutputter.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/JsonInfosetOutputter.scala
@@ -100,7 +100,7 @@ class JsonInfosetOutputter private (writer: java.io.Writer, pretty: Boolean, dum
   override def startSimple(simple: DISimple): Boolean = {
     startNode()
     startElement(simple)
-    if (!isNilled(simple)) {
+    if (!isNilled(simple) && simple.hasValue) {
       val text =
         if (simple.erd.optPrimType.get.isInstanceOf[NodeInfo.String.Kind]) {
           new String(stringEncoder.quoteAsString(simple.dataValueAsString)) // escapes according to Json spec
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/SAXInfosetOutputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/SAXInfosetOutputter.scala
new file mode 100644
index 0000000..d786db7
--- /dev/null
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/SAXInfosetOutputter.scala
@@ -0,0 +1,303 @@
+/*
+ * 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
+
+import java.io.OutputStream
+import java.io.OutputStreamWriter
+
+import scala.xml.NamespaceBinding
+
+import org.apache.daffodil.api.DFDL
+import org.apache.daffodil.dpath.NodeInfo
+import org.apache.daffodil.util.Indentable
+import org.apache.daffodil.util.MStackOfBoolean
+import org.apache.daffodil.xml.XMLUtils
+import org.xml.sax.Attributes
+import org.xml.sax.ContentHandler
+import org.xml.sax.Locator
+import org.xml.sax.SAXException
+import org.xml.sax.helpers.AttributesImpl
+
+class SAXInfosetOutputter(xmlReader: DFDL.DaffodilXMLReader)
+  extends InfosetOutputter
+  with XMLInfosetOutputter {
+  /**
+   * Reset the internal state of this InfosetOutputter. This should be called
+   * inbetween calls to the parse method.
+   */
+  override def reset(): Unit = {
+    // this doesn't do anything as the ContentHandler API does not support
+    // resetting, but some implemented ContentHandlers, such as the JDOM SaxHandler,
+    // do support resetting so it's up to the creator of the contentHandler, to call
+    // their contentHandler's reset if applicable and if necessary
+  }
+
+  override def startDocument(): Boolean = {
+    val contentHandler = xmlReader.getContentHandler
+    try {
+      contentHandler.startDocument()
+      true
+    } catch {
+      case _: SAXException => false
+    }
+  }
+
+  override def endDocument(): Boolean = {
+    val contentHandler = xmlReader.getContentHandler
+    try {
+      contentHandler.endDocument()
+      true
+    } catch {
+      case _: SAXException => false
+    }
+  }
+
+  override def startSimple(diSimple: DISimple): Boolean = {
+    val contentHandler = xmlReader.getContentHandler
+    try {
+      doStartElement(diSimple, contentHandler)
+      if (diSimple.hasValue) {
+        val text =
+          if (diSimple.erd.optPrimType.get.isInstanceOf[NodeInfo.String.Kind]) {
+            val s = remapped(diSimple.dataValueAsString)
+            scala.xml.Utility.escape(s)
+          } else {
+            diSimple.dataValueAsString
+          }
+        val arr = text.toCharArray
+        contentHandler.characters(arr, 0, arr.length)
+      }
+      true
+    } catch {
+      case _: SAXException => false
+    }
+  }
+
+  override def endSimple(diSimple: DISimple): Boolean = {
+    val contentHandler = xmlReader.getContentHandler
+    try {
+      doEndElement(diSimple, contentHandler)
+      true
+    } catch {
+      case _: SAXException => false
+    }
+  }
+
+  override def startComplex(diComplex: DIComplex): Boolean = {
+    val contentHandler = xmlReader.getContentHandler
+    try {
+      doStartElement(diComplex, contentHandler)
+      true
+    } catch {
+      case _: SAXException => false
+    }
+  }
+
+  override def endComplex(diComplex: DIComplex): Boolean = {
+    val contentHandler = xmlReader.getContentHandler
+    try {
+      doEndElement(diComplex, contentHandler)
+      true
+    } catch {
+      case _: SAXException => false
+    }
+  }
+
+  override def startArray(diArray: DIArray): Boolean = true // not applicable
+
+  override def endArray(diArray: DIArray): Boolean = true // not applicable
+
+  private def createNilAttribute(): AttributesImpl = {
+    val attrs = new AttributesImpl()
+    attrs.addAttribute(XMLUtils.XSI_NAMESPACE, "nil", "xsi:nil", "", "true")
+    attrs
+  }
+
+  private def doStartPrefixMapping(diElem: DIElement, contentHandler: ContentHandler): Unit = {
+    val nsbStart = diElem.erd.minimizedScope
+    val nsbEnd = if (diElem.isRoot) {
+      scala.xml.TopScope
+    } else {
+      diElem.diParent.erd.minimizedScope
+    }
+    var n = nsbStart
+    while (n != nsbEnd) {
+      val prefix = if (n.prefix == null) "" else n.prefix
+      val uri = if (n.uri == null) "" else n.uri
+      contentHandler.startPrefixMapping(prefix, uri)
+      n = n.parent
+    }
+  }
+
+  private def doEndPrefixMapping(diElem: DIElement, contentHandler: ContentHandler): Unit = {
+    val nsbStart = diElem.erd.minimizedScope
+    val nsbEnd = if (diElem.isRoot) {
+      scala.xml.TopScope
+    } else {
+      diElem.diParent.erd
+        .minimizedScope
+    }
+    var n = nsbStart
+    while (n != nsbEnd) {
+      val prefix = if (n.prefix == null) "" else n.prefix
+      contentHandler.endPrefixMapping(prefix)
+      n = n.parent
+    }
+  }
+
+  private def doStartElement(diElem: DIElement, contentHandler: ContentHandler): Unit = {
+    val (ns: String, elemName: String, qName: String) = getNameSpaceElemNameAndQName(diElem)
+    doStartPrefixMapping(diElem, contentHandler)
+
+    val attrs = if (isNilled(diElem)) {
+      createNilAttribute()
+    } else {
+      new AttributesImpl()
+    }
+
+    contentHandler.startElement(ns, elemName, qName, attrs)
+  }
+
+  private def doEndElement (diElem: DIElement, contentHandler: ContentHandler): Unit = {
+    val (ns: String, elemName: String, qName: String) = getNameSpaceElemNameAndQName(diElem)
+    contentHandler.endElement(ns, elemName, qName)
+    doEndPrefixMapping(diElem, contentHandler)
+  }
+
+  private def getNameSpaceElemNameAndQName(
+    diElem: DIElement): (String, String, String) = {
+    val ns: String =
+      if (diElem.erd.namedQName.namespace.isNoNamespace) {
+        ""
+      } else {
+        diElem.erd.namedQName.namespace.toString
+      }
+    val elemName = diElem.erd.namedQName.local
+    val qName = diElem.erd.namedQName.toQNameString
+    (ns, elemName, qName)
+  }
+
+}
+
+class DaffodilOutputContentHandler(out: OutputStream, pretty: Boolean = false)
+  extends ContentHandler with Indentable with XMLInfosetOutputter {
+  private val writer = new OutputStreamWriter(out)
+  private var prefixMapping: NamespaceBinding = null
+  private val outputNewlineStack: MStackOfBoolean = {
+    val s = MStackOfBoolean()
+    s.push(false)
+    s
+  }
+
+  // if the top of the stack is true, we have guessed we should output a newline
+  private def outputNewline: Boolean = outputNewlineStack.top
+
+  def reset(): Unit = {
+    resetIndentation()
+    writer.flush()
+    prefixMapping = null
+    outputNewlineStack.clear()
+    outputNewlineStack.push(false) //to match initialization state
+    out.flush()
+  }
+
+  override def setDocumentLocator(locator: Locator): Unit = {
+    // do nothing
+  }
+
+  override def startDocument(): Unit = {
+    writer.write("""<?xml version="1.0" encoding="UTF-8"?>""")
+  }
+
+  override def endDocument(): Unit = {
+    writer.write(System.lineSeparator())
+    writer.flush()
+  }
+
+  override def startPrefixMapping(prefix: String, uri: String): Unit = {
+    val _prefix = if (prefix == "") null else prefix
+    prefixMapping = NamespaceBinding(_prefix, uri, prefixMapping)
+  }
+
+  override def endPrefixMapping(prefix: String): Unit = {
+  // do nothing
+  }
+
+  override def startElement(uri: String, localName: String, qName: String, atts: Attributes): Unit = {
+    // the pop/true removes whatever is on the stack which is our previous guess for whether we
+    // would need a newline after the previous end tag. As we are currently at the start of a new
+    // tag, we want to correct that assumption (in case it was false)
+    outputNewlineStack.pop()
+    outputNewlineStack.push(true)
+    if (pretty) {
+      writer.write(System.lineSeparator())
+      outputIndentation(writer)
+    }
+    // handle start of tag
+    writer.write("<")
+    writer.write(qName)
+    // handle attributes
+    for (i <- 0 until atts.getLength) {
+      val attsValue = atts.getValue(i)
+      val attsQName = atts.getQName(i)
+      writer.write(s""" $attsQName="$attsValue"""")
+    }
+    // handle namespaces
+    if (prefixMapping != null) {
+      val pm = prefixMapping.toString()
+      writer.write(pm)
+      prefixMapping = null
+    }
+    // handle end of tag
+    writer.write(">")
+    incrementIndentation()
+    // this push makes the assumption that we would not need to output a newline after this end
+    // tag is complete
+    outputNewlineStack.push(false)
+  }
+
+  override def endElement(uri: String, localName: String,  qName: String): Unit = {
+    decrementIndentation()
+    if (outputNewline) {
+      if (pretty) {
+        writer.write(System.lineSeparator())
+        outputIndentation(writer)
+      }
+    }
+    writer.write("</")
+    writer.write(qName)
+    writer.write(">")
+    outputNewlineStack.pop()
+  }
+
+  override def characters(ch: Array[Char], start: Int, length: Int): Unit = {
+    writer.write(ch, start, length)
+  }
+
+  override def ignorableWhitespace(ch: Array[Char], start: Int, length: Int): Unit = {
+    // do nothing
+  }
+
+  override def processingInstruction(target: String, data: String): Unit = {
+    // do nothing
+  }
+
+  override def skippedEntity(name: String): Unit = {
+    // do nothing
+  }
+}
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/ScalaXMLInfosetOutputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/ScalaXMLInfosetOutputter.scala
index ab46fd6..b75a9a1 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/ScalaXMLInfosetOutputter.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/ScalaXMLInfosetOutputter.scala
@@ -52,8 +52,8 @@ class ScalaXMLInfosetOutputter(showFormatInfo: Boolean = false) extends InfosetO
 
     val e =
       if (isNilled(diSimple)) {
-        scala.xml.Elem(diSimple.erd.thisElementsNamespacePrefix, diSimple.erd.name,
-            XMLUtils.xmlNilAttribute, diSimple.erd.minimizedScope, true)
+        scala.xml.Elem(diSimple.erd.namedQName.prefixOrNull, diSimple.erd.name,
+            XMLUtils.xmlNilAttribute, diSimple.erd.minimizedScope, minimizeEmpty = true)
       } else if (diSimple.hasValue) {
         val text =
           if (diSimple.erd.optPrimType.get.isInstanceOf[NodeInfo.String.Kind]) {
@@ -62,12 +62,12 @@ class ScalaXMLInfosetOutputter(showFormatInfo: Boolean = false) extends InfosetO
             diSimple.dataValueAsString
           }
         val textNode = new scala.xml.Text(text)
-        scala.xml.Elem(diSimple.erd.thisElementsNamespacePrefix, diSimple.erd.name, Null,
-            diSimple.erd.minimizedScope, true, textNode)
+        scala.xml.Elem(diSimple.erd.namedQName.prefixOrNull, diSimple.erd.name, Null,
+          diSimple.erd.minimizedScope, minimizeEmpty = true, textNode)
       } else {
         // element has been created but has no value yet, display an empty element tag
-        scala.xml.Elem(diSimple.erd.thisElementsNamespacePrefix, diSimple.erd.name, Null,
-            diSimple.erd.minimizedScope, true)
+        scala.xml.Elem(diSimple.erd.namedQName.prefixOrNull, diSimple.erd.name, Null,
+            diSimple.erd.minimizedScope, minimizeEmpty = true)
       }
 
     val elem = addFmtInfo(diSimple, e, showFormatInfo)
@@ -92,11 +92,11 @@ class ScalaXMLInfosetOutputter(showFormatInfo: Boolean = false) extends InfosetO
 
     val e =
       if (isNilled(diComplex)) {
-        scala.xml.Elem(diComplex.erd.thisElementsNamespacePrefix, diComplex.erd.name,
-          XMLUtils.xmlNilAttribute, diComplex.erd.minimizedScope, true)
+        scala.xml.Elem(diComplex.erd.namedQName.prefixOrNull, diComplex.erd.name,
+          XMLUtils.xmlNilAttribute, diComplex.erd.minimizedScope, minimizeEmpty = true)
       } else {
-        scala.xml.Elem(diComplex.erd.thisElementsNamespacePrefix, diComplex.erd.name,
-            scala.xml.Null, diComplex.erd.minimizedScope, true, children: _*)
+        scala.xml.Elem(diComplex.erd.namedQName.prefixOrNull, diComplex.erd.name,
+            scala.xml.Null, diComplex.erd.minimizedScope,  minimizeEmpty = true, children: _*)
       }
 
     val elem = addFmtInfo(diComplex, e, showFormatInfo)
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/W3CDOMInfosetOutputter.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/W3CDOMInfosetOutputter.scala
index faf0cf2..2bb809e 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/W3CDOMInfosetOutputter.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/infoset/W3CDOMInfosetOutputter.scala
@@ -109,10 +109,10 @@ class W3CDOMInfosetOutputter extends InfosetOutputter
     assert(document != null)
 
     val elem: Element =
-      if(diElement.erd.thisElementsNamespace.isNoNamespace) {
+      if(diElement.erd.namedQName.namespace.isNoNamespace) {
         document.createElementNS(null, diElement.erd.name)
       } else {
-        document.createElementNS(diElement.erd.thisElementsNamespace, diElement.erd.prefixedName)
+        document.createElementNS(diElement.erd.namedQName.namespace, diElement.erd.prefixedName)
       }
 
     if (isNilled(diElement)) {
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 6dbd1a0..a2487dc 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
@@ -48,7 +48,7 @@ class XMLTextInfosetOutputter private (writer: java.io.Writer, pretty: Boolean,
   }
 
   private def outputTagName(elem: DIElement): Unit = {
-    val prefix = elem.erd.thisElementsNamespacePrefix
+    val prefix = elem.erd.namedQName.prefixOrNull
     if (prefix != null && prefix != "") {
       writer.write(prefix)
       writer.write(":")
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DataProcessor.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DataProcessor.scala
index 0522f2d..80fe696 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DataProcessor.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/DataProcessor.scala
@@ -19,19 +19,21 @@ package org.apache.daffodil.processors
 
 import java.io.File
 import java.io.IOException
+import java.io.InputStream
 import java.io.ObjectOutputStream
 import java.nio.CharBuffer
 import java.nio.LongBuffer
 import java.nio.channels.Channels
 import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
 import java.util.zip.GZIPOutputStream
 
 import scala.collection.immutable.Queue
+import scala.collection.mutable
 
-import org.xml.sax.ErrorHandler
-import org.xml.sax.SAXException
-import org.xml.sax.SAXParseException
-
+import org.apache.daffodil.Implicits._; object INoWarn4 {
+  ImplicitsSuppressUnusedImportWarning() }
 import org.apache.daffodil.api.DFDL
 import org.apache.daffodil.api.DaffodilTunables
 import org.apache.daffodil.api.ValidationMode
@@ -39,9 +41,11 @@ import org.apache.daffodil.api.WithDiagnostics
 import org.apache.daffodil.debugger.Debugger
 import org.apache.daffodil.dsom.TunableLimitExceededError
 import org.apache.daffodil.dsom._
-import org.apache.daffodil.equality._; object EqualityNoWarn3 { EqualitySuppressUnusedImportWarning() }
+import org.apache.daffodil.equality._; object EqualityNoWarn3 {
+  EqualitySuppressUnusedImportWarning() }
 import org.apache.daffodil.events.MultipleEventHandler
 import org.apache.daffodil.exceptions.Assert
+import org.apache.daffodil.exceptions.SchemaFileLocation
 import org.apache.daffodil.exceptions.UnsuppressableException
 import org.apache.daffodil.externalvars.Binding
 import org.apache.daffodil.externalvars.ExternalVariablesLoader
@@ -50,6 +54,7 @@ import org.apache.daffodil.infoset.InfosetElement
 import org.apache.daffodil.infoset.InfosetException
 import org.apache.daffodil.infoset.InfosetInputter
 import org.apache.daffodil.infoset.InfosetOutputter
+import org.apache.daffodil.infoset.SAXInfosetOutputter
 import org.apache.daffodil.infoset.TeeInfosetOutputter
 import org.apache.daffodil.infoset.XMLTextInfosetOutputter
 import org.apache.daffodil.io.BitOrderChangeException
@@ -68,6 +73,17 @@ import org.apache.daffodil.util.Maybe
 import org.apache.daffodil.util.Maybe._
 import org.apache.daffodil.util.Misc
 import org.apache.daffodil.util.Validator
+import org.apache.daffodil.xml.XMLUtils
+import org.xml.sax.ContentHandler
+import org.xml.sax.DTDHandler
+import org.xml.sax.EntityResolver
+import org.xml.sax.ErrorHandler
+import org.xml.sax.InputSource
+import org.xml.sax.SAXException
+import org.xml.sax.SAXNotRecognizedException
+import org.xml.sax.SAXNotSupportedException
+import org.xml.sax.SAXParseException
+
 
 /**
  * Implementation mixin - provides simple helper methods
@@ -337,6 +353,8 @@ class DataProcessor private (
 
   override def getDiagnostics = ssrd.diagnostics
 
+  override def newXMLReaderInstance: DFDL.DaffodilXMLReader = new DaffodilXMLReader(this)
+
   def save(output: DFDL.Output): Unit = {
 
     externalVars.foreach{ ev =>
@@ -735,3 +753,168 @@ class UnparseResult(dp: DataProcessor, ustate: UState)
     encodingInfo.knownEncodingName
   }
 }
+
+class DaffodilXMLReader(dp: DataProcessor) extends DFDL.DaffodilXMLReader {
+  private var contentHandler: ContentHandler = _
+  private var errorHandler: ErrorHandler = _
+  private var dtdHandler: DTDHandler = _
+  private var entityResolver: EntityResolver = _
+  private val saxNamespaceFeature = "http://xml.org/sax/features/namespaces"
+  private val saxNamespacePrefixFeature = "http://xml.org/sax/features/namespace-prefixes"
+  var saxParseResultProperty: ParseResult = _
+  var saxBlobDirectoryProperty: Path = Paths.get(System.getProperty("java.io.tmpdir"))
+  var saxBlobPrefixProperty: String = "daffodil-sax-"
+  var saxBlobSuffixProperty: String = ".blob"
+
+  private val featureMap = mutable.Map[String, Boolean](saxNamespaceFeature -> false,
+    saxNamespacePrefixFeature -> false)
+
+  override def getFeature(name: String): Boolean = {
+    if (name == saxNamespaceFeature || name == saxNamespacePrefixFeature) {
+      featureMap(name)
+    } else {
+      throw new SAXNotRecognizedException("Feature unsupported: " + name + ".\n" +
+        "Supported features are: " + featureMap.keys.mkString(", "))
+      false
+    }
+  }
+
+  override def setFeature(name: String, value: Boolean): Unit = {
+    if (name == saxNamespaceFeature || name == saxNamespacePrefixFeature) {
+      featureMap(name) = value
+    } else {
+      throw new SAXNotRecognizedException("Feature unsupported: " + name + ".\n" +
+        "Supported features are: " + featureMap.keys.mkString(", "))
+    }
+  }
+
+  override def getProperty(name: String): AnyRef = {
+    val prop = name match {
+      case XMLUtils.DAFFODIL_SAX_URN_PARSERESULT => saxParseResultProperty
+      case XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY => saxBlobDirectoryProperty
+      case XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX => saxBlobPrefixProperty
+      case XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX => saxBlobSuffixProperty
+      case _ =>
+        throw new SAXNotRecognizedException("Property unsupported: " + name + ".")
+    }
+    prop
+  }
+
+  override def setProperty(name: String, value: AnyRef): Unit = {
+    try {
+      name match {
+        case XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY => saxBlobDirectoryProperty =
+          value.asInstanceOf[Path]
+        case XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX => saxBlobPrefixProperty = value
+          .asInstanceOf[String]
+        case XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX => saxBlobSuffixProperty = value
+          .asInstanceOf[String]
+        case _ =>
+          throw new SAXNotRecognizedException("Property unsupported: " + name + ".")
+      }
+    } catch {
+      case _: ClassCastException =>
+        throw new SAXNotSupportedException("Unsupported value for property: " + name + "." )
+    }
+  }
+
+  override def setEntityResolver(resolver: EntityResolver): Unit = {
+    entityResolver = resolver
+  }
+
+  override def getEntityResolver: EntityResolver = entityResolver
+
+  override def setDTDHandler(handler: DTDHandler): Unit = {
+    dtdHandler = handler
+  }
+
+  override def getDTDHandler: DTDHandler = dtdHandler
+
+  override def setContentHandler(handler: ContentHandler): Unit = {
+    contentHandler = handler;
+  }
+
+  override def getContentHandler: ContentHandler = contentHandler
+
+  override def setErrorHandler(handler: ErrorHandler): Unit = {
+    errorHandler = handler;
+  }
+
+  override def getErrorHandler: ErrorHandler = errorHandler
+
+  override def parse(input: InputSource): Unit = {
+    val is = input.getByteStream
+    if (is != null) {
+      val isdis = InputSourceDataInputStream(is)
+      parse(isdis)
+    } else {
+      throw new IOException("Inputsource must be backed by Inputstream")
+    }
+  }
+
+  override def parse(systemId: String): Unit = {
+    throw new IOException("SAX parsing of systemId is unsupported")
+  }
+
+  def parse(isdis: InputSourceDataInputStream): Unit = {
+    val sio = createSAXInfosetOutputter(this)
+    val pr = dp.parse(isdis, sio)
+    handleDiagnostics(pr)
+    saxParseResultProperty = pr .asInstanceOf[ParseResult]
+  }
+
+  def parse(stream: InputStream): Unit = {
+    val isdis = InputSourceDataInputStream(stream)
+    parse(isdis)
+  }
+
+  def parse(arr: Array[Byte]): Unit = {
+    val isdis = InputSourceDataInputStream(arr)
+    parse(isdis)
+  }
+
+  private def handleDiagnostics(pr: DFDL.ParseResult): Unit = {
+    val diagnostics = pr.getDiagnostics
+    val eh = this.getErrorHandler
+    if (diagnostics.nonEmpty && eh != null) {
+      diagnostics.foreach { d =>
+        val spe = {
+          val msg = d.getMessage()
+          val (lineNo, colNo, systemId) = d.getLocationsInSchemaFiles.headOption.map { s =>
+            val sl = s.asInstanceOf[SchemaFileLocation]
+            val ln = sl.lineNumber.getOrElse("0").toInt
+            val cn = sl.columnNumber.getOrElse("0").toInt
+            val sId = sl.uriString
+            (ln, cn, sId)
+          }.getOrElse((0,0, null))
+
+          val spe = new SAXParseException(msg, null, systemId, lineNo, colNo, d)
+          spe
+        }
+
+        if (d.isError) {
+          eh.error(spe)
+        } else {
+          eh.warning(spe)
+        }
+      }
+    }
+  }
+
+  /**
+   * Creates SAXInfosetOutputter object and attempts to setBlobAttributes on it if
+   * it has at least the blobDirectory property set
+   *
+   * @return SAXInfosetOutputter object with or without blob Attributes set
+   */
+  private def createSAXInfosetOutputter(xmlReader: DaffodilXMLReader): SAXInfosetOutputter = {
+    val sioo = new SAXInfosetOutputter(xmlReader)
+    val siof = try {
+      sioo.setBlobAttributes(saxBlobDirectoryProperty, saxBlobPrefixProperty, saxBlobSuffixProperty)
+      sioo
+    } catch {
+      case e: SAXNotSupportedException => sioo
+    }
+    siof
+  }
+}
diff --git a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/RuntimeData.scala b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/RuntimeData.scala
index 3afca03..aa6138c 100644
--- a/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/RuntimeData.scala
+++ b/daffodil-runtime1/src/main/scala/org/apache/daffodil/processors/RuntimeData.scala
@@ -561,7 +561,6 @@ sealed class ElementRuntimeData(
   defaultBitOrderArg: BitOrder,
   @TransientParam optPrimTypeArg: => Option[PrimType],
   @TransientParam targetNamespaceArg: => NS,
-  @TransientParam thisElementsNamespaceArg: => NS,
   @TransientParam optSimpleTypeRuntimeDataArg: => Option[SimpleTypeRuntimeData],
   @TransientParam optComplexTypeModelGroupRuntimeDataArg: => Option[ModelGroupRuntimeData],
   @TransientParam minOccursArg: => Long,
@@ -569,7 +568,6 @@ sealed class ElementRuntimeData(
   @TransientParam maybeOccursCountKindArg: => Maybe[OccursCountKind],
   @TransientParam nameArg: => String,
   @TransientParam targetNamespacePrefixArg: => String,
-  @TransientParam thisElementsNamespacePrefixArg: => String,
   @TransientParam isNillableArg: => Boolean,
   @TransientParam isArrayArg: => Boolean, // can have more than 1 occurrence
   @TransientParam isOptionalArg: => Boolean, // can have only 0 or 1 occurrence
@@ -617,7 +615,6 @@ sealed class ElementRuntimeData(
   lazy val minimizedScope = minimizedScopeArg
   lazy val optPrimType = optPrimTypeArg
   lazy val targetNamespace = targetNamespaceArg
-  lazy val thisElementsNamespace = thisElementsNamespaceArg
   lazy val optSimpleTypeRuntimeData = optSimpleTypeRuntimeDataArg
   lazy val optComplexTypeModelGroupRuntimeData = optComplexTypeModelGroupRuntimeDataArg
   lazy val minOccurs = minOccursArg
@@ -625,7 +622,6 @@ sealed class ElementRuntimeData(
   lazy val maybeOccursCountKind = maybeOccursCountKindArg
   lazy val name = nameArg
   lazy val targetNamespacePrefix = targetNamespacePrefixArg
-  lazy val thisElementsNamespacePrefix = thisElementsNamespacePrefixArg
   lazy val isNillable = isNillableArg
   override lazy val isArray = isArrayArg
   lazy val isOptional = isOptionalArg
@@ -648,7 +644,6 @@ sealed class ElementRuntimeData(
     minimizedScope
     optPrimType
     targetNamespace
-    thisElementsNamespace
     optSimpleTypeRuntimeData
     optComplexTypeModelGroupRuntimeData
     minOccurs
@@ -656,7 +651,6 @@ sealed class ElementRuntimeData(
     maybeOccursCountKind
     name
     targetNamespacePrefix
-    thisElementsNamespacePrefix
     isNillable
     isArray
     isOptional
@@ -685,8 +679,8 @@ sealed class ElementRuntimeData(
   def isComplexType = !isSimpleType
 
   def prefixedName = {
-    if (thisElementsNamespacePrefix != null) {
-      thisElementsNamespacePrefix + ":" + name
+    if (namedQName.prefixOrNull != null) {
+      namedQName.prefixOrNull + ":" + name
     } else {
       name
     }
@@ -744,7 +738,6 @@ sealed abstract class ErrorERD(local: String, namespaceURI: String)
     null, //defaultBitOrderArg: => BitOrder,
     None, // optPrimTypeArg: => Option[PrimType],
     null, // targetNamespaceArg: => NS,
-    NS(namespaceURI), // thisElementsNamespaceArg: => NS,
     null, // optSimpleTypeRuntimeDataArg: => Option[SimpleTypeRuntimeData],
     null, // optComplexTypeModelGroupRuntimeDataArg: => Option[ModelGroupRuntimeData],
     0L, // minOccursArg: => Long,
@@ -752,7 +745,6 @@ sealed abstract class ErrorERD(local: String, namespaceURI: String)
     Nope, // maybeOccursCountKindArg: => Maybe[OccursCountKind],
     local, // nameArg: => String,
     null, // targetNamespacePrefixArg: => String,
-    null, // thisElementsNamespacePrefixArg: => String,
     false, // isNillableArg: => Boolean,
     false, // isArrayArg: => Boolean, // can have more than 1 occurrence
     false, // isOptionalArg: => Boolean, // can have only 0 or 1 occurrence
@@ -816,7 +808,7 @@ final class NamespaceAmbiguousElementErrorERD(
    * is what was expected.
    */
   def toUnparseError(nothingWasExpected: Boolean = false) = {
-    val sqn = StepQName(None, name, thisElementsNamespace)
+    val sqn = StepQName(None, name, namedQName.namespace)
     val sqnx = sqn.toExtendedSyntax
     val allPossiblesString =
       allPossibleNQNs.map { _.toExtendedSyntax }.mkString(", ")
diff --git a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/Daffodil.scala b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/Daffodil.scala
index 35b0016..c9946ba 100644
--- a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/Daffodil.scala
+++ b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/Daffodil.scala
@@ -25,7 +25,6 @@ import org.apache.daffodil.sapi.infoset._
 import org.apache.daffodil.sapi.io.InputSourceDataInputStream
 import org.apache.daffodil.debugger.{ InteractiveDebugger => SInteractiveDebugger }
 import org.apache.daffodil.debugger.{ TraceDebuggerRunner => STraceDebuggerRunner }
-import org.apache.daffodil.api.{ Diagnostic => SDiagnostic }
 import java.io.File
 import java.nio.channels.Channels
 import java.nio.channels.ReadableByteChannel
@@ -37,6 +36,7 @@ import org.apache.daffodil.api.{ LocationInSchemaFile => SLocationInSchemaFile }
 import org.apache.daffodil.api.{ WithDiagnostics => SWithDiagnostics }
 import org.apache.daffodil.compiler.{ ProcessorFactory => SProcessorFactory }
 import org.apache.daffodil.processors.{ DataProcessor => SDataProcessor }
+import org.apache.daffodil.processors.{ DaffodilXMLReader => SDaffodilXMLReader }
 import org.apache.daffodil.processors.{ ParseResult => SParseResult }
 import org.apache.daffodil.processors.{ UnparseResult => SUnparseResult }
 import org.apache.daffodil.util.{ ConsoleWriter => SConsoleWriter }
@@ -619,6 +619,12 @@ class DataProcessor private[sapi] (private var dp: SDataProcessor)
   def save(output: WritableByteChannel): Unit = dp.save(output)
 
   /**
+   *  Obtain a new [[DaffodilXMLReader]] from the current [[DataProcessor]].
+   */
+  def newXMLReaderInstance: DaffodilXMLReader =
+    new DaffodilXMLReader(xmlrdr = dp.newXMLReaderInstance.asInstanceOf[SDaffodilXMLReader])
+
+  /**
    * Parse input data with a specified length
    *
    * @param input data to be parsed
@@ -790,3 +796,120 @@ class InvalidParserException(cause: org.apache.daffodil.compiler.InvalidParserEx
  * This exception will be thrown as a result of an invalid usage of the Daffodil API
  */
 class InvalidUsageException(cause: org.apache.daffodil.processors.InvalidUsageException) extends Exception(cause.getMessage(), cause.getCause())
+
+/**
+ * SAX Method of parsing schema and getting the DFDL Infoset via designated
+ * org.xml.sax.ContentHandler, based on the org.xml.sax.XMLReader interface
+ */
+class DaffodilXMLReader private[sapi] (xmlrdr: SDaffodilXMLReader) extends org.xml.sax.XMLReader {
+  /**
+   * Get the value of the feature flag
+   * @param name feature flag whose value is to be retrieved
+   * @return value of the feature flag
+   */
+  override def getFeature(name: String): Boolean = xmlrdr.getFeature(name)
+
+  /**
+   * Set the value of the feature flag
+   * @param name feature flag to be set
+   * @param value value to be assigned to feature flag
+   */
+  override def setFeature(name: String, value: Boolean): Unit = xmlrdr.setFeature(name, value)
+
+  /**
+   * Get the value of the property
+   * @param name property whose value is to be retrieved
+   * @return value of the property
+   */
+  override def getProperty(name: String): AnyRef = xmlrdr.getProperty(name)
+
+  /**
+   * Set the value of the property
+   * @param name property whose value is to be set
+   * @param value value to be assigned to the property
+   */
+  override def setProperty(name: String, value: AnyRef): Unit = xmlrdr.setProperty(name, value)
+
+  /**
+   * Register an entity resolver
+   * @param resolver entity resolver to be registered
+   */
+  override def setEntityResolver(resolver: org.xml.sax.EntityResolver): Unit =
+    xmlrdr.setEntityResolver(resolver)
+
+  /**
+   * Return the registered entity resolver
+   * @return registered entity resolver or null
+   */
+  override def getEntityResolver: org.xml.sax.EntityResolver = xmlrdr.getEntityResolver
+
+  /**
+   * Register a DTD Handler
+   * @param handler DTD Handler to be registered
+   */
+  override def setDTDHandler(handler: org.xml.sax.DTDHandler): Unit = xmlrdr.setDTDHandler(handler)
+
+  /**
+   * Retrieve registered DTD Handler
+   * @return registered DTD Handler or null
+   */
+  override def getDTDHandler: org.xml.sax.DTDHandler = xmlrdr.getDTDHandler
+
+  /**
+   * Register a content handler
+   * @param handler content handler to be registered
+   */
+  override def setContentHandler(handler: org.xml.sax.ContentHandler): Unit = xmlrdr.setContentHandler(handler)
+
+  /**
+   * Retrieve registered content handler
+   * @return registered content handler or null
+   */
+  override def getContentHandler: org.xml.sax.ContentHandler = xmlrdr.getContentHandler
+
+  /**
+   * Register an error handler
+   * @param handler error handler to be registered
+   */
+  override def setErrorHandler(handler: org.xml.sax.ErrorHandler): Unit = xmlrdr.setErrorHandler(handler)
+
+  /**
+   * Retrieve registered error handler
+   * @return registered error handler or null
+   */
+  override def getErrorHandler: org.xml.sax.ErrorHandler = xmlrdr.getErrorHandler
+
+  /**
+   * Parse input data from an InputSource. Infoset can be retrieved via the registered
+   * contentHandler and diagnostics via the registered errorHandler
+   * @param input data to be parsed
+   */
+  override def parse(input: org.xml.sax.InputSource): Unit = xmlrdr.parse(input)
+
+  /**
+   * Parse data from a system identifier/URI. This method is not supported by the API.
+   * @param systemId URI for data to be parsed
+   */
+  override def parse(systemId: String): Unit = xmlrdr.parse(systemId)
+
+  /**
+   * Parse input data from an InputSourceDataInputStream. Infoset can retrieved via the registered
+   * contentHandler and diagnostics via the registered errorHandler
+   * @param isdis data to be parsed
+   */
+  def parse(isdis: InputSourceDataInputStream): Unit = xmlrdr.parse(isdis.dis)
+
+  /**
+   * Parse input data from an InputStream. Infoset can retrieved via the registered contentHandler
+   * and diagnostics via the registered errorHandler
+   * @param stream data to be parsed
+   */
+  def parse(stream: java.io.InputStream): Unit = xmlrdr.parse(stream)
+
+  /**
+   * Parse input data from an array of bytes. Infoset can retrieved via the registered
+   * contentHandler and diagnostics via the registered errorHandler
+   * @param arr data to be parsed
+   */
+  def parse(arr: Array[Byte]): Unit = xmlrdr.parse(arr)
+}
diff --git a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/package.scala b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/package.scala
index 47e1b20..2d20e64 100644
--- a/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/package.scala
+++ b/daffodil-sapi/src/main/scala/org/apache/daffodil/sapi/package.scala
@@ -25,7 +25,7 @@ package org.apache.daffodil
  * <h3>Overview</h3>
  *
  * The [[Daffodil]] object is a factory object to create a [[Compiler]]. The
- * [[Compiler]] provides a method to compils a provided DFDL schema into a
+ * [[Compiler]] provides a method to compile a provided DFDL schema into a
  * [[ProcessorFactory]], which creates a [[DataProcessor]]:
  *
  * {{{
@@ -37,10 +37,59 @@ package org.apache.daffodil
  * The [[DataProcessor]] provides the necessary functions to parse and unparse
  * data, returning a [[ParseResult]] or [[UnparseResult]], respectively. These
  * contain information about the parse/unparse, such as whether or not the
- * processing succeeded any diagnostic information.
+ * processing succeeded with any diagnostic information.
+ *
+ * The [[DataProcessor]] also provides a function to create a [[DaffodilXMLReader]] that can be
+ * used to perform parsing via the SAX API.
+ *
+ * {{{
+ * val xmlRdr = dp.newXMLReaderInstance
+ * }}}
+ *
+ * The [[DaffodilXMLReader]] has several methods that allow one to set properties and handlers
+ * (such as ContentHandlers or ErrorHandlers) for the reader. One can use any
+ * contentHandler/errorHandler as long as they extend the org.xml.sax.ContentHandler and
+ * org.xml.sax.ErrorHandler interfaces respectively. One can use any contentHandler/errorHandler
+ * as long as they extend the org.xml.sax.ContentHandler and org.xml.sax.ErrorHandler interfaces
+ * respectively. One can also set properties for the [[DaffodilXMLReader]] using
+ * [[DaffodilXMLReader.setProperty(name:String* DaffodilXMLReader.setProperty]].
+ *
+ * The following properties can be set as follows:
+ * {{{
+ * xmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY, "/tmp/")
+ * xmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX, "daffodil-sax-")
+ * xmlRdr.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX, ".bin")
+ * }}}
+ * The variables above start with "urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:sax:" and end
+ * with BlobDirectory, BlobPrefix and BlobSuffix respectively.
+ *
+ * The properties can be retrieved using the same variables with
+ * [[DaffodilXMLReader.getProperty(name:String* DaffodilXMLReader.getProperty]]
+ *
+ * The following handlers can be set as follows:
+ * {{{
+ * xmlRdr.setContentHandler(contentHandler)
+ * xmlRdr.setErrorHandler(errorHandler)
+ * xmlRdr.setDTDHandler(dtdHandler)
+ * xmlRdr.setEntityResolver(entityResolver)
+ * }}}
+ *
+ * The handlers above must implement the following interfaces respectively:
+ * {{{
+ * org.xml.sax.ContentHandler
+ * org.xml.sax.ErrorHandler
+ * org.xml.sax.DTDHandler
+ * org.xml.sax.EntityResolver
+ * }}}
+ *
+ * The [[ParseResult]] can be found as a property within the [[DaffodilXMLReader]] using this
+ * uri: "urn:ogf:dfdl:2013:imp:daffodil.apache.org:2018:sax:ParseResult" or
+ * XMLUtils.DAFFODIL_SAX_URN_PARSERESULT
  *
  * <h4>Parse</h4>
  *
+ * <h5>Dataprocessor Parse</h5>
+ *
  * The [[DataProcessor.parse(input:org\.apache\.daffodil\.sapi\.io\.InputSourceDataInputStream* DataProcessor.parse]] method accepts input data to parse in the form
  * of a [[io.InputSourceDataInputStream InputSourceDataInputStream]] and an [[infoset.InfosetOutputter InfosetOutputter]]
  * to determine the output representation of the infoset (e.g. Scala XML Nodes,
@@ -83,6 +132,59 @@ package org.apache.daffodil
  * }
  * }}}
  *
+ * <h5>SAX Parse</h5>
+ *
+ * The [[DaffodilXMLReader.parse(isdis:org\.apache\.daffodil\.sapi\.io\.InputSourceDataInputStream* DaffodilXMLReader.parse]] method accepts input data to parse in the form of a
+ * [[io.InputSourceDataInputStream InputSourceDataInputStream]]. The output representation of the
+ * infoset, as well as how parse errors are handled, are dependent on the content handler and the
+ * error handler provided to the [[DaffodilXMLReader]]. For example the
+ * org.jdom2.input.sax.SAXHandler provides a JDOM representation, whereas other Content
+ * Handlers may output directly to an java.io.OutputStream or java.io.Writer.
+ *
+ * {{{
+ * val contentHandler = new SAXHandler()
+ * xmlRdr.setContentHandler(contentHandler)
+ * val is = new InputSourceDataInputStream(data)
+ * xmlReader.parse(is)
+ * val pr = xmlRdr.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT)
+ * val doc = saxHandler.getDocument
+ * }}}
+ *
+ * The [[DaffodilXMLReader.parse(isdis:org\.apache\.daffodil\.sapi\.io\.InputSourceDataInputStream* DaffodilXMLReader.parse]] method is not thread-safe and may only be called again/reused once a
+ * parse operation is completed. This can be done multiple times without the need to create new
+ * DaffodilXMLReaders, ContentHandlers or ErrorHandlers. It might be necessary to reset whatever
+ * ContentHandler is used (or allocate a new one). A thread-safe implementation would require
+ * unique instances of the DaffodilXMLReader and its components. For example:
+ *
+ * {{{
+ * val contentHandler = new SAXHandler()
+ * xmlRdr.setContentHandler(contentHandler)
+ * files.foreach { f => {
+ *   contentHandler.reset
+ *   val is = new InputSourceDataInputStream(new FileInputStream(f))
+ *   xmlReader.parse(is)
+ *   val pr = xmlRdr.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT)
+ *   val doc = saxHandler.getDocument
+ * }
+ * }}}
+ *
+ * One can repeat calls to parse() using the same InputSourceDataInputStream to continue parsing
+ * where the previous parse ended. For example:
+ *
+ * {{{
+ * val is = new InputSourceDataInputStream(dataStream)
+ * val contentHandler = new SAXHandler()
+ * xmlRdr.setContentHandler(contentHandler)
+ * val keepParsing = true
+ * while (keepParsing) {
+ *   contentHandler.reset()
+ *   xmlRdr.parse(is)
+ *   val pr = xmlRdr.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT)
+ *   ...
+ *   keepParsing = !pr.location().isAtEnd() && !pr.isError()
+ * }
+ * }}}
+ *
  * <h4>Unparse</h4>
  *
  * The same [[DataProcessor]] used for parse can be used to unparse an infoset
@@ -132,9 +234,22 @@ package org.apache.daffodil
  *
  * {{{
  * val dp = Daffodil.reload(saveFile);
+ * }}}
+ *
+ * And use like below:
+ * {{{
  * val pr = dp.parse(data, inputter);
  * }}}
  *
+ * or
+ *
+ * {{{
+ * val xmlRdr = dp.newXMLReaderInstance
+ * ... // setting appropriate handlers
+ * xmlReader.parse(data)
+ * val pr = xmlRdr.getProperty("...ParseResult")
+ * }}}
+ *
  */
 package object sapi
 
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 0e49291..2e84525 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
@@ -27,6 +27,8 @@ import java.io.ObjectInputStream
 import java.io.ObjectOutputStream
 import java.io.File
 import java.nio.channels.Channels
+import java.nio.file.Paths
+
 import org.junit.Test
 import org.apache.daffodil.sapi.Daffodil
 import org.apache.daffodil.sapi.DataProcessor
@@ -39,6 +41,11 @@ import org.apache.daffodil.sapi.infoset.ScalaXMLInfosetOutputter
 import org.apache.daffodil.sapi.infoset.ScalaXMLInfosetInputter
 import org.apache.daffodil.sapi.io.InputSourceDataInputStream
 import org.apache.daffodil.exceptions.Abort
+import org.apache.daffodil.infoset.DaffodilOutputContentHandler
+import org.apache.daffodil.processors.{ ParseResult => SParseResult }
+import org.apache.daffodil.sapi.SAXErrorHandlerForSAPITest
+import org.apache.daffodil.sapi.infoset.XMLTextInfosetOutputter
+import org.apache.daffodil.xml.XMLUtils
 
 class TestScalaAPI {
 
@@ -144,7 +151,6 @@ class TestScalaAPI {
     // reset the global logging and debugger state
     Daffodil.setLogWriter(new ConsoleLogWriter())
     Daffodil.setLoggingLevel(LogLevel.Info)
-
   }
 
   // This is a duplicate of test testScalaAPI1 that serializes the parser
@@ -971,4 +977,88 @@ class TestScalaAPI {
     }
   }
 
+  @Test
+  def testScalaAPI20(): Unit = {
+    /** Test SAX parsing */
+    val output = new ByteArrayOutputStream
+
+    val c = Daffodil.compiler()
+
+    val schemaFile = getResource("/test/sapi/mySchema1.dfdl.xsd")
+    val pf = c.compileFile(schemaFile)
+    val dp1 = pf.onPath("/")
+    val dp = reserializeDataProcessor(dp1)
+    val xri = dp.newXMLReaderInstance
+
+    // val file = getResource("/test/sapi/myDataBroken.dat")
+    val file = getResource("/test/sapi/myData.dat")
+    val fisDP = new java.io.FileInputStream(file)
+    val fisSAX = new java.io.FileInputStream(file)
+    val inputSAX = new InputSourceDataInputStream(fisSAX)
+    val inputDP = new InputSourceDataInputStream(fisDP)
+    val bosDP = new ByteArrayOutputStream()
+    val outputter = new XMLTextInfosetOutputter(bosDP, pretty = true)
+    dp.parse(inputDP, outputter)
+    val infosetDPString = bosDP.toString()
+
+    val bos = new ByteArrayOutputStream()
+    val contentHandler = new DaffodilOutputContentHandler(bos, pretty = true)
+    val errorHandler = new SAXErrorHandlerForSAPITest()
+    xri.setContentHandler(contentHandler)
+    xri.setErrorHandler(errorHandler)
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY,
+      Paths.get(System.getProperty("java.io.tmpdir")))
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX, "daffodil-sapi-")
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX, ".sax.blob")
+    xri.parse(inputSAX)
+    val resSAX = xri.getProperty(XMLUtils.DAFFODIL_SAX_URN_PARSERESULT)
+      .asInstanceOf[SParseResult]
+    val err = errorHandler.isError
+    val diags = errorHandler.getDiagnostics
+    val infosetSAXString = bos.toString
+
+    assertFalse(err)
+    assertTrue(resSAX.resultState.currentLocation.isAtEnd)
+    assertTrue(diags.isEmpty)
+    assertEquals(infosetDPString, infosetSAXString)
+  }
+
+  @Test
+  def testScalaAPI21(): Unit = {
+    /** Test parse with errors */
+    val c = Daffodil.compiler()
+
+    val schemaFile = getResource("/test/sapi/mySchema1.dfdl.xsd")
+    val pf = c.compileFile(schemaFile)
+    val dp1 = pf.onPath("/")
+    val dp = reserializeDataProcessor(dp1)
+    val xri = dp.newXMLReaderInstance
+
+    val file = getResource("/test/sapi/myDataBroken.dat")
+    val fis = new java.io.FileInputStream(file)
+    val input = new InputSourceDataInputStream(fis)
+    val bos = new ByteArrayOutputStream()
+    val contentHandler = new DaffodilOutputContentHandler(bos)
+    val errorHandler = new SAXErrorHandlerForSAPITest()
+    xri.setContentHandler(contentHandler)
+    xri.setErrorHandler(errorHandler)
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY,
+      Paths.get(System.getProperty("java.io.tmpdir")))
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX, "daffodil-sapi-")
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX, ".sax.blob")
+    xri.parse(input)
+
+    assertTrue(errorHandler.isError)
+    val diags = errorHandler.getDiagnostics
+    assertEquals(1, diags.size)
+    val d = diags.head
+    assertTrue(d.getMessage().contains("int"))
+    assertTrue(d.getMessage().contains("Not an int"))
+    assertTrue(d.getDataLocations.toString().contains("10"))
+    val locs = d.getLocationsInSchemaFiles
+    assertEquals(1, locs.size)
+    val loc = locs.head
+    assertTrue(loc.toString().contains("mySchema1.dfdl.xsd")) // reports the element ref, not element decl.
+  }
+
 }
diff --git a/daffodil-sapi/src/test/scala/org/apache/daffodil/sapi/SAXErrorHandlerForSAPITest.scala b/daffodil-sapi/src/test/scala/org/apache/daffodil/sapi/SAXErrorHandlerForSAPITest.scala
new file mode 100644
index 0000000..774731f
--- /dev/null
+++ b/daffodil-sapi/src/test/scala/org/apache/daffodil/sapi/SAXErrorHandlerForSAPITest.scala
@@ -0,0 +1,47 @@
+/*
+ * 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
+
+import org.apache.daffodil.api.{Diagnostic => SDiagnostic}
+import org.xml.sax.ErrorHandler
+import org.xml.sax.SAXParseException
+
+class SAXErrorHandlerForSAPITest extends ErrorHandler {
+    private var _diagnostics: Seq[Diagnostic] = Nil
+    private var _isError: Boolean = false
+
+    override def warning(exception: SAXParseException): Unit = {
+      _isError = false
+      val embeddedDiagnostic = new Diagnostic(exception.getCause.asInstanceOf[SDiagnostic])
+      _diagnostics :+= embeddedDiagnostic
+    }
+
+    override def error(exception: SAXParseException): Unit = {
+      _isError = true
+      val embeddedDiagnostic = new Diagnostic(exception.getCause.asInstanceOf[SDiagnostic])
+      _diagnostics :+= embeddedDiagnostic
+    }
+
+    override def fatalError(exception: SAXParseException): Unit = {
+      error(exception)
+    }
+
+    def getDiagnostics: Seq[Diagnostic] = _diagnostics
+
+    def isError: Boolean = _isError
+  }
diff --git a/daffodil-tdml-lib/src/main/scala/org/apache/daffodil/tdml/TDMLRunner.scala b/daffodil-tdml-lib/src/main/scala/org/apache/daffodil/tdml/TDMLRunner.scala
index fff99d7..9e2a3c1 100644
--- a/daffodil-tdml-lib/src/main/scala/org/apache/daffodil/tdml/TDMLRunner.scala
+++ b/daffodil-tdml-lib/src/main/scala/org/apache/daffodil/tdml/TDMLRunner.scala
@@ -17,16 +17,29 @@
 
 package org.apache.daffodil.tdml
 
+import java.io.ByteArrayInputStream
 import java.io.File
 import java.io.FileNotFoundException
+import java.io.InputStream
+import java.io.OutputStream
 import java.net.URI
+import java.nio.ByteBuffer
+import java.nio.CharBuffer
+import java.nio.LongBuffer
+import java.nio.charset.CoderResult
+import java.nio.charset.{Charset => JavaCharset}
+import java.nio.file.Files
 
-import scala.xml.Node
+import scala.language.postfixOps
+import scala.util.Try
 import scala.xml.Elem
+import scala.xml.Node
 import scala.xml.NodeSeq
 import scala.xml.NodeSeq.seqToNodeSeq
 import scala.xml.SAXParseException
 import scala.xml.transform._
+
+import org.apache.commons.io.IOUtils
 import org.apache.daffodil.api.DaffodilSchemaSource
 import org.apache.daffodil.api.DaffodilTunables
 import org.apache.daffodil.api.DataLocation
@@ -35,56 +48,40 @@ import org.apache.daffodil.api.EmbeddedSchemaSource
 import org.apache.daffodil.api.URISchemaSource
 import org.apache.daffodil.api.UnitTestSchemaSource
 import org.apache.daffodil.api.ValidationMode
+import org.apache.daffodil.configuration.ConfigurationLoader
+import org.apache.daffodil.cookers.EntityReplacer
 import org.apache.daffodil.exceptions.Assert
 import org.apache.daffodil.exceptions.UnsuppressableException
 import org.apache.daffodil.externalvars.Binding
-import org.apache.daffodil.util.LogLevel
-import org.apache.daffodil.util.Logging
-import org.apache.daffodil.util.Misc
-import org.apache.daffodil.util.Misc.bits2Bytes
-import org.apache.daffodil.util.Misc.hex2Bits
-import org.apache.daffodil.util.SchemaUtils
-import org.apache.daffodil.xml.DaffodilXMLLoader
-import org.apache.daffodil.xml._
+import org.apache.daffodil.io.FormatInfo
+import org.apache.daffodil.io.InputSourceDataInputStream
+import org.apache.daffodil.processors.HasSetDebugger
+import org.apache.daffodil.processors.charset.BitsCharsetDecoder
+import org.apache.daffodil.processors.charset.BitsCharsetEncoder
+import org.apache.daffodil.processors.charset.BitsCharsetNonByteSize
+import org.apache.daffodil.processors.charset.BitsCharsetNonByteSizeEncoder
 import org.apache.daffodil.processors.charset.CharsetUtils
-import java.nio.ByteBuffer
-import java.nio.CharBuffer
-import java.nio.LongBuffer
-import java.nio.charset.CoderResult
-import java.io.ByteArrayInputStream
-
-import scala.language.postfixOps
-import java.nio.file.Files
-import java.io.InputStream
-
-import org.apache.commons.io.IOUtils
-import org.apache.daffodil.cookers.EntityReplacer
-import org.apache.daffodil.configuration.ConfigurationLoader
+import org.apache.daffodil.schema.annotation.props.gen.BinaryFloatRep
 import org.apache.daffodil.schema.annotation.props.gen.BitOrder
 import org.apache.daffodil.schema.annotation.props.gen.ByteOrder
-import org.apache.daffodil.schema.annotation.props.gen.BinaryFloatRep
 import org.apache.daffodil.schema.annotation.props.gen.EncodingErrorPolicy
 import org.apache.daffodil.schema.annotation.props.gen.UTF16Width
-import org.apache.daffodil.util.MaybeInt
-import org.apache.daffodil.util.Maybe
-import org.apache.daffodil.processors.charset.BitsCharsetDecoder
-import org.apache.daffodil.processors.charset.BitsCharsetEncoder
-import org.apache.daffodil.processors.charset.BitsCharsetNonByteSize
-import org.apache.daffodil.processors.charset.BitsCharsetNonByteSizeEncoder
-import org.apache.daffodil.io.FormatInfo
-import org.apache.daffodil.io.InputSourceDataInputStream
-import java.nio.charset.{Charset => JavaCharset}
-import java.io.OutputStream
-
-import org.apache.daffodil.tdml.processor.TDMLParseResult
+import org.apache.daffodil.tdml.processor.AbstractTDMLDFDLProcessorFactory
+import org.apache.daffodil.tdml.processor.TDML
 import org.apache.daffodil.tdml.processor.TDMLDFDLProcessor
+import org.apache.daffodil.tdml.processor.TDMLParseResult
 import org.apache.daffodil.tdml.processor.TDMLResult
 import org.apache.daffodil.tdml.processor.TDMLUnparseResult
-import org.apache.daffodil.tdml.processor.TDML
-import org.apache.daffodil.tdml.processor.AbstractTDMLDFDLProcessorFactory
-
-import scala.util.Try
-import org.apache.daffodil.processors.HasSetDebugger
+import org.apache.daffodil.util.LogLevel
+import org.apache.daffodil.util.Logging
+import org.apache.daffodil.util.Maybe
+import org.apache.daffodil.util.MaybeInt
+import org.apache.daffodil.util.Misc
+import org.apache.daffodil.util.Misc.bits2Bytes
+import org.apache.daffodil.util.Misc.hex2Bits
+import org.apache.daffodil.util.SchemaUtils
+import org.apache.daffodil.xml.DaffodilXMLLoader
+import org.apache.daffodil.xml.XMLUtils
 
 /**
  * Parses and runs tests expressed in IBM's contributed tdml "Test Data Markup Language"
@@ -860,7 +857,7 @@ case class ParserTestCase(ptc: NodeSeq, parentArg: DFDLTestSuite)
       if (processor.isError || optExtVarDiag.isDefined) {
         val noParseResult: TDMLParseResult = null
         val allDiags: Seq[Diagnostic] = processor.getDiagnostics ++ optExtVarDiag
-        (noParseResult, allDiags , true)
+        (noParseResult, allDiags, true)
       } else {
         val actual =
           try {
diff --git a/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/tdml/processor/DaffodilTDMLDFDLProcessor.scala b/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/tdml/processor/DaffodilTDMLDFDLProcessor.scala
index e8d50fb..6e941d1 100644
--- a/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/tdml/processor/DaffodilTDMLDFDLProcessor.scala
+++ b/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/tdml/processor/DaffodilTDMLDFDLProcessor.scala
@@ -17,12 +17,15 @@
 
 package org.apache.daffodil.tdml.processor
 
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
 import java.nio.channels.Channels
 import java.nio.file.Path
 import java.nio.file.Paths
 
 import scala.xml.Node
 
+import org.apache.commons.io.IOUtils
 import org.apache.daffodil.api._
 import org.apache.daffodil.compiler.Compiler
 import org.apache.daffodil.debugger.Debugger
@@ -31,15 +34,21 @@ import org.apache.daffodil.debugger.TraceDebuggerRunner
 import org.apache.daffodil.dsom.ExpressionCompilers
 import org.apache.daffodil.exceptions.Assert
 import org.apache.daffodil.externalvars.Binding
+import org.apache.daffodil.infoset.DaffodilOutputContentHandler
 import org.apache.daffodil.infoset.ScalaXMLInfosetInputter
 import org.apache.daffodil.io.InputSourceDataInputStream
 import org.apache.daffodil.processors.DataProcessor
 import org.apache.daffodil.processors.UnparseResult
 import org.apache.daffodil.processors.unparsers.UState
 import org.apache.daffodil.tdml.SchemaDataProcessorCache
+import org.apache.daffodil.tdml.TDMLException
 import org.apache.daffodil.tdml.TDMLInfosetInputter
 import org.apache.daffodil.tdml.TDMLInfosetOutputter
 import org.apache.daffodil.util.MaybeULong
+import org.apache.daffodil.xml.XMLUtils
+import org.apache.daffodil.xml.XMLUtils.XMLDifferenceException
+import org.xml.sax.ErrorHandler
+import org.xml.sax.SAXParseException
 
 final class TDMLDFDLProcessorFactory private (
   private var compiler: Compiler,
@@ -234,21 +243,22 @@ class DaffodilTDMLDFDLProcessor private (private var dp: DataProcessor) extends
 
   override def getDiagnostics: Seq[Diagnostic] = dp.getDiagnostics
 
-  override def parse(is: java.io.InputStream, lengthLimitInBits: Long): TDMLParseResult = {
-
-    val outputter = new TDMLInfosetOutputter()
-    outputter.setBlobAttributes(blobDir, blobPrefix, blobSuffix)
+  def parse(uri: java.net.URI, lengthLimitInBits: Long): TDMLParseResult = {
+    val url = uri.toURL
+    val dpInputStream = url.openStream()
+    val saxInputStream = url.openStream()
+    doParseWithBothApis(dpInputStream, saxInputStream, lengthLimitInBits)
+  }
 
-    val dis = InputSourceDataInputStream(is)
-    if (lengthLimitInBits >= 0 && lengthLimitInBits % 8 != 0) {
-      // Only set the bit limit if the length is not a multiple of 8. In that
-      // case, we aren't expected to consume all the data and need a bitLimit
-      // to prevent messages about left over bits.
-      dis.setBitLimit0b(MaybeULong(lengthLimitInBits))
-    }
-    val actual = dp.parse(dis, outputter)
+  def parse(arr: Array[Byte], lengthLimitInBits: Long): TDMLParseResult = {
+    val dpInputStream = new ByteArrayInputStream(arr)
+    val saxInputStream = new ByteArrayInputStream(arr)
+    doParseWithBothApis(dpInputStream, saxInputStream, lengthLimitInBits)
+  }
 
-    new DaffodilTDMLParseResult(actual, outputter)
+  override def parse(is: java.io.InputStream, lengthLimitInBits: Long): TDMLParseResult = {
+    val arr = IOUtils.toByteArray(is)
+    parse(arr, lengthLimitInBits)
   }
 
   override def unparse(infosetXML: scala.xml.Node, outStream: java.io.OutputStream): TDMLUnparseResult = {
@@ -274,6 +284,80 @@ class DaffodilTDMLDFDLProcessor private (private var dp: DataProcessor) extends
     new DaffodilTDMLUnparseResult(actual, outStream)
   }
 
+  def doParseWithBothApis(dpInputStream: java.io.InputStream, saxInputStream: java.io.InputStream,
+    lengthLimitInBits: Long): TDMLParseResult = {
+    val outputter = new TDMLInfosetOutputter()
+    outputter.setBlobAttributes(blobDir, blobPrefix, blobSuffix)
+
+    val xri = dp.newXMLReaderInstance
+    val errorHandler = new DaffodilTDMLSAXErrorHandler()
+    val outputStream = new ByteArrayOutputStream()
+    val saxHandler = new DaffodilOutputContentHandler(outputStream, pretty = true)
+    xri.setContentHandler(saxHandler)
+    xri.setErrorHandler(errorHandler)
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBDIRECTORY, blobDir)
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBPREFIX, blobPrefix)
+    xri.setProperty(XMLUtils.DAFFODIL_SAX_URN_BLOBSUFFIX, blobSuffix)
+
+    val dis = InputSourceDataInputStream(dpInputStream)
+    val sis = InputSourceDataInputStream(saxInputStream)
+    if (lengthLimitInBits >= 0 && lengthLimitInBits % 8 != 0) {
+      // Only set the bit limit if the length is not a multiple of 8. In that
+      // case, we aren't expected to consume all the data and need a bitLimit
+      // to prevent messages about left over bits.
+      dis.setBitLimit0b(MaybeULong(lengthLimitInBits))
+      sis.setBitLimit0b(MaybeULong(lengthLimitInBits))
+    }
+    xri.parse(sis)
+    val actual = dp.parse(dis, outputter)
+
+
+    if (!actual.isError && !errorHandler.isError) {
+      verifySameParseOutput(outputter, outputStream)
+    }
+    verifySameDiagnostics(actual, errorHandler)
+
+    new DaffodilTDMLParseResult(actual, outputter)
+  }
+
+  def verifySameParseOutput(dpOutputter: TDMLInfosetOutputter, outputStream: ByteArrayOutputStream): Unit = {
+    val dpParseOutput = dpOutputter.getResult()
+    val saxParseOutputString = outputStream.toString
+    val saxParseOutput = scala.xml.XML.loadString(saxParseOutputString)
+
+    try {
+      XMLUtils.compareAndReport(
+        dpParseOutput,
+        saxParseOutput,
+        checkNamespaces = true,
+        checkPrefixes = true)
+    } catch {
+      case e: XMLDifferenceException => {
+        throw TDMLException(
+          """SAX parse output (actual) does not match DataProcessor Parse output (expected)""" +
+            "\n" + e.getMessage, None)
+      }
+    }
+  }
+
+  private def verifySameDiagnostics(
+    actual: DFDL.ParseResult,
+    errorHandler: DaffodilTDMLSAXErrorHandler): Unit = {
+    val dpParseDiag = actual.getDiagnostics.map(_.getMessage()).sorted
+    val saxParseDiag = errorHandler.getDiagnostics.map(_.getMessage()).sorted
+
+    if (dpParseDiag != saxParseDiag) {
+      throw TDMLException(
+        """SAX parse diagnostics do not match DataProcessor Parse diagnostics""" +
+          "\n" +
+          """DataProcessor Parse diagnostics: """ + dpParseDiag +
+          (if (saxParseDiag.isEmpty) {
+            "\n No SAX diagnostics were generated."
+          } else {
+            "\n SAX Parse diagnostics: " + saxParseDiag
+          }), None)
+    }
+  }
 }
 
 final class DaffodilTDMLParseResult(actual: DFDL.ParseResult, outputter: TDMLInfosetOutputter) extends TDMLParseResult {
@@ -313,3 +397,28 @@ final class DaffodilTDMLUnparseResult(actual: DFDL.UnparseResult, outStream: jav
 
   override def bitPos0b: Long = ustate.bitPos0b
 }
+
+class DaffodilTDMLSAXErrorHandler extends ErrorHandler with WithDiagnostics {
+  private var diagnostics: Seq[Diagnostic] = Nil
+  private var errorStatus: Boolean = false
+
+  override def warning(exception: SAXParseException): Unit = {
+    errorStatus = false
+    val embeddedDiagnostic = exception.getCause.asInstanceOf[Diagnostic]
+    diagnostics :+= embeddedDiagnostic
+  }
+
+  override def error(exception: SAXParseException): Unit = {
+    errorStatus = true
+    val embeddedDiagnostic = exception.getCause.asInstanceOf[Diagnostic]
+    diagnostics :+= embeddedDiagnostic
+  }
+
+  override def fatalError(exception: SAXParseException): Unit = {
+    error(exception)
+  }
+
+  override def getDiagnostics: Seq[Diagnostic] = diagnostics
+
+  override def isError: Boolean = errorStatus
+}
diff --git a/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml b/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
index 6bf91cf..66f5f8f 100644
--- a/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
+++ b/daffodil-test/src/test/resources/org/apache/daffodil/section07/variables/variables.tdml
@@ -802,7 +802,7 @@
     <tdml:errors>
       <tdml:error>Schema Definition Error</tdml:error>
       <tdml:error>newVariableInstances must all be distinct within the same scope</tdml:error>
-      <tdml:error>ex:myVar1</tdml:error>
+      <tdml:error>myVar1</tdml:error>
     </tdml:errors>
 
   </tdml:parserTestCase>