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

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


The following commit(s) were added to refs/heads/main by this push:
     new b8976946e Fix ProcessorFactory.withDistinguishedRootNode and isError
b8976946e is described below

commit b8976946ec02c0475ce665b527db3d6c7014dba9
Author: Steve Lawrence <[email protected]>
AuthorDate: Tue Nov 28 14:12:30 2023 -0500

    Fix ProcessorFactory.withDistinguishedRootNode and isError
    
    When calling withDistinguishedRootNode, we copy the ProcessorFactory and
    replace the root spec with the new node. When we do this, we also copy
    the existing SchemaSet. But this SchemaSet was created with the previous
    root spec, which means withDistinguishedRootNode doesn't actually do
    anything.
    
    Fortunately, there is a workaround which is to pass in the root
    name/namspace to the compileSource/File functions, so that the root spec
    is available when the SchemaSet is created, and avoid
    withDistinguishedRootNode entirely.
    
    But to fix this, this modifies withDistinguishedRootNode to not copy the
    SchemaSet, so a new SchemaSet is created and initialized with the
    correct root spec. Each ProcessorFactory now has a unique SchemaSet
    instance with the correct root name and namespace.
    
    This also discovered cases in the Compiler.compileSourceInternal and
    JAPI/SAPI compileFile functions where they called isError on the newly
    created ProcessorFactory before returning it to the caller. This meant
    that the SchemaSet inside the ProcessorFactory would be forced to be
    created, even though it might never be used, like in the case of
    withDistinguishedRootNode being called and a different SchemaSet being
    created.
    
    Really the isError function should only ever be called by the user when
    they are ready for the object to be evaluated, and not forced by
    Daffodil internals. This ensures we avoid unnecessary work until the
    user is ready, since isError is what eventually causes lazy vals to be
    evaluated. However, this does mean that if a user doesn't check isError
    they might get exceptions/assertions, but it is well documented in the
    JAPI/SAPI that isError should be checked before calling other functions.
    
    This also means that some debug logging that relies on knowing that
    status of isError must be moved elsewhere, since the user might not have
    called isError yet.
    
    This also discovered instances in the TDMLRunner and CLI where we
    queried diagnostics before checking isError--isError should always be
    done first, which is fixed.
    
    That said, this also modifies ProcessorFactory.getDiagnostics so that it
    calls isError before returning the diagnostics. This way if user calls
    getDiagnostics without first calling isError (like we used to do in the
    TDMLRunner and CLI), the compilation work will be done and correct
    diagnostics generated. Without this, there would be no diagnostics
    because no work would be done yet. Although technically a user should
    call isError before asking for diagnostics, there may be cases where
    users don't, so this allows that to keep working as expected. This is
    similar to how onPath calls isError even if a user doesn't.
    
    DAFFODIL-2864, DAFFODIL-697
---
 .../main/scala/org/apache/daffodil/cli/Main.scala  | 14 +++++-
 .../apache/daffodil/core/compiler/Compiler.scala   | 38 ++++++---------
 .../core/runtime1/SchemaSetRuntime1Mixin.scala     |  5 --
 .../scala/org/apache/daffodil/japi/Daffodil.scala  |  1 -
 .../org/apache/daffodil/example/TestJavaAPI.java   | 55 ++++++++++++++++++++++
 .../scala/org/apache/daffodil/sapi/Daffodil.scala  |  1 -
 .../org/apache/daffodil/example/TestScalaAPI.scala | 52 ++++++++++++++++++++
 .../processor/tdml/DaffodilTDMLDFDLProcessor.scala |  2 +-
 8 files changed, 136 insertions(+), 32 deletions(-)

diff --git a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala 
b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
index 2162fc3a7..b30db8871 100644
--- a/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
+++ b/daffodil-cli/src/main/scala/org/apache/daffodil/cli/Main.scala
@@ -72,6 +72,7 @@ import 
org.apache.daffodil.runtime1.debugger.TraceDebuggerRunner
 import org.apache.daffodil.runtime1.externalvars.ExternalVariablesLoader
 import org.apache.daffodil.runtime1.layers.LayerExecutionException
 import org.apache.daffodil.runtime1.processors.DataLoc
+import org.apache.daffodil.runtime1.processors.DataProcessor
 import org.apache.daffodil.runtime1.processors.ExternalVariableException
 import org.apache.daffodil.runtime1.udf.UserDefinedFunctionFatalErrorException
 import org.apache.daffodil.slf4j.DaffodilLogger
@@ -960,6 +961,11 @@ class Main(
       // saved processor. Those are from compile time, are all warnings, and
       // are just noise in a production system where we're reloading a parser.
       if (!processor.isError) {
+        Logger.log.whenDebugEnabled {
+          val processorImpl = processor.asInstanceOf[DataProcessor]
+          Logger.log.debug(s"Parser = ${processorImpl.ssrd.parser.toString}")
+          Logger.log.debug(s"Unparser = 
${processorImpl.ssrd.unparser.toString}")
+        }
         Some(processor.withValidationMode(mode))
       } else {
         None
@@ -1024,10 +1030,16 @@ class Main(
         val processorFactory = compiler.compileSource(schemaSource)
         if (!processorFactory.isError) {
           val processor = 
processorFactory.onPath(path.getOrElse("/")).withValidationMode(mode)
-          displayDiagnostics(processor)
           if (processor.isError) {
+            displayDiagnostics(processor)
             None
           } else {
+            displayDiagnostics(processor)
+            Logger.log.whenDebugEnabled {
+              val processorImpl = processor.asInstanceOf[DataProcessor]
+              Logger.log.debug(s"Parser = 
${processorImpl.ssrd.parser.toString}")
+              Logger.log.debug(s"Unparser = 
${processorImpl.ssrd.unparser.toString}")
+            }
             Some(processor)
           }
         } else {
diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala
index d6a05556d..75ec86408 100644
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala
+++ 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/compiler/Compiler.scala
@@ -38,7 +38,6 @@ import org.apache.daffodil.lib.api.Diagnostic
 import org.apache.daffodil.lib.api.URISchemaSource
 import org.apache.daffodil.lib.api.UnitTestSchemaSource
 import org.apache.daffodil.lib.exceptions.Assert
-import org.apache.daffodil.lib.util.Logger
 import org.apache.daffodil.lib.util.Misc
 import org.apache.daffodil.runtime1.api.DFDL
 import org.apache.daffodil.runtime1.processors.DataProcessor
@@ -65,7 +64,6 @@ final class ProcessorFactory private (
   val validateDFDLSchemas: Boolean,
   checkAllTopLevel: Boolean,
   tunables: DaffodilTunables,
-  optSchemaSet: Option[SchemaSet],
 ) extends DFDL.ProcessorFactory {
 
   def this(
@@ -82,7 +80,6 @@ final class ProcessorFactory private (
       validateDFDLSchemas,
       checkAllTopLevel,
       tunables,
-      None,
     )
 
   private def copy(optRootSpec: Option[RootSpec] = optRootSpec): 
ProcessorFactory =
@@ -92,19 +89,29 @@ final class ProcessorFactory private (
       validateDFDLSchemas,
       checkAllTopLevel,
       tunables,
-      Some(sset),
     )
 
   lazy val sset: SchemaSet =
-    optSchemaSet.getOrElse(
-      SchemaSet(optRootSpec, schemaSource, validateDFDLSchemas, 
checkAllTopLevel, tunables),
-    )
+    SchemaSet(optRootSpec, schemaSource, validateDFDLSchemas, 
checkAllTopLevel, tunables)
 
   lazy val rootView: RootView = sset.root
 
   def elementBaseInstanceCount: Long = sset.elementBaseInstanceCount
 
-  def diagnostics: Seq[Diagnostic] = sset.diagnostics
+  def diagnostics: Seq[Diagnostic] = {
+    // The work to compile a schema and build diagnostics is triggered by the 
user calling
+    // isError. But if a user gets diagnostics before doing so, then no work 
will have been done
+    // and the diagnostics will be empty. Technically this is incorrect 
usage--a user should
+    // always call isError before getting diagnostics. But there are known 
instances where users
+    // have done this. We could detect this and throw a usage assertion so 
users know to fix it,
+    // but if they want diagnostics then they likely expected the work to have 
been done
+    // already, so lets just call isError to trigger that work so they get 
what they expect.
+    // Note that we don't check the result of isError, since it is perfectly 
reasonable for
+    // errors to exist when a user asks for diagnostics--we only call it for 
its side-effects.
+    isError
+    sset.diagnostics
+  }
+
   def getDiagnostics: Seq[Diagnostic] = diagnostics
 
   override def onPath(xpath: String): DFDL.DataProcessor = sset.onPath(xpath)
@@ -359,21 +366,6 @@ class Compiler private (
       )
     }
 
-    val err = pf.isError
-    val diags = pf.getDiagnostics // might be warnings even if not isError
-    if (err) {
-      Assert.invariant(diags.nonEmpty)
-      Logger.log.debug(
-        s"Compilation (ProcessorFactory) produced ${diags.length} 
errors/warnings.",
-      )
-    } else {
-      if (diags.nonEmpty) {
-        Logger.log.debug(s"Compilation (ProcessorFactory) produced 
${diags.length} warnings.")
-      } else {
-        Logger.log.debug(s"ProcessorFactory completed with no errors.")
-      }
-    }
-    Logger.log.debug(s"Schema had ${pf.elementBaseInstanceCount} elements.")
     pf
   }
 
diff --git 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
index 7e26c0a27..4e9ce52b1 100644
--- 
a/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
+++ 
b/daffodil-core/src/main/scala/org/apache/daffodil/core/runtime1/SchemaSetRuntime1Mixin.scala
@@ -72,11 +72,6 @@ trait SchemaSetRuntime1Mixin {
       )
     val dataProc =
       new DataProcessor(ssrd, tunable, variableMap.copy(), diagnostics = 
this.diagnostics)
-    if (dataProc.isError) {} else {
-      Logger.log.debug(s"Parser = ${ssrd.parser.toString}.")
-      Logger.log.debug(s"Unparser = ${ssrd.unparser.toString}.")
-      Logger.log.debug(s"Compilation (DataProcesor) completed with no errors.")
-    }
     dataProc
   }
 }
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 0542badfb..ae33e22e1 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
@@ -141,7 +141,6 @@ class Compiler private[japi] (private var sCompiler: 
SCompiler) {
   ): ProcessorFactory = {
 
     val pf = sCompiler.compileFile(schemaFile, Option(rootName), 
Option(rootNamespace))
-    pf.isError
     new ProcessorFactory(pf)
   }
 
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 1ad39bd5c..4ba616953 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
@@ -1346,4 +1346,59 @@ public class TestJavaAPI {
             Files.delete(blobDir);
         }
     }
+
+    /**
+     * Verify that ProcessorFactory.withDistinguishedRootNode selects the 
right node
+     */
+    @Test
+    public void testJavaAPIWithDistinguishedRootNode() throws IOException, 
ClassNotFoundException {
+        org.apache.daffodil.japi.Compiler c = Daffodil.compiler();
+
+        // e3 is defined first in mySchema3.dfdl.xsd, so if 
withDistinguishedRootNode is ignored,
+        // this should give a different result
+        java.io.File schemaFile = getResource("/test/japi/mySchema3.dfdl.xsd");
+        ProcessorFactory pf = c.compileFile(schemaFile);
+        pf = pf.withDistinguishedRootNode("e4", null);
+        DataProcessor dp = pf.onPath("/");
+
+        java.io.File file = getResource("/test/japi/myData16.dat");
+        java.io.FileInputStream fis = new java.io.FileInputStream(file);
+        InputSourceDataInputStream dis = new InputSourceDataInputStream(fis);
+        JDOMInfosetOutputter outputter = new JDOMInfosetOutputter();
+        ParseResult res = dp.parse(dis, outputter);
+        boolean err = res.isError();
+        assertFalse(err);
+        assertEquals(5, res.location().bytePos1b());
+        assertEquals(33, res.location().bitPos1b());
+
+        java.io.ByteArrayOutputStream bos = new 
java.io.ByteArrayOutputStream();
+        java.nio.channels.WritableByteChannel wbc = 
java.nio.channels.Channels.newChannel(bos);
+        JDOMInfosetInputter inputter = new 
JDOMInfosetInputter(outputter.getResult());
+        UnparseResult res2 = dp.unparse(inputter, wbc);
+        err = res2.isError();
+        assertFalse(err);
+        assertEquals("9100", bos.toString());
+    }
+
+    /***
+     * Verify that a user can get diagnostics without having to call isError
+     *
+     * @throws IOException
+     */
+    @Test
+    public void testJavaAPIGetDiagnostics() throws IOException {
+        org.apache.daffodil.japi.Compiler c = Daffodil.compiler();
+        java.io.File schemaFile = new 
java.io.File("/test/japi/notHere1.dfdl.xsd");
+        ProcessorFactory pf = c.compileFile(schemaFile);
+        List<Diagnostic> diags = pf.getDiagnostics();
+        boolean found1 = false;
+        for (Diagnostic d : diags) {
+            if (d.getMessage().contains("notHere1")) {
+                found1 = true;
+            }
+        }
+        assertTrue(found1);
+        assertTrue(pf.isError());
+    }
+
 }
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 805babdcc..7eec0f333 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
@@ -136,7 +136,6 @@ class Compiler private[sapi] (private var sCompiler: 
SCompiler) {
     optRootNamespace: Option[String] = None,
   ): ProcessorFactory = {
     val pf = sCompiler.compileFile(schemaFile, optRootName, optRootNamespace)
-    pf.isError
     new ProcessorFactory(pf)
   }
 
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 14b31031b..43f551eca 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
@@ -1325,4 +1325,56 @@ class TestScalaAPI {
       Files.delete(blobDir)
     }
   }
+
+  /**
+   * Verify that ProcessorFactory.withDistinguishedRootNode selects the right 
node
+   */
+  @Test
+  def testScalaAPIWithDistinguishedRootNode(): Unit = {
+    val c = Daffodil.compiler()
+
+    // e3 is defined first in mySchema3.dfdl.xsd, so if 
withDistinguishedRootNode is ignored,
+    // this should give a different result
+    val schemaFile = getResource("/test/sapi/mySchema3.dfdl.xsd")
+    val pf = c
+      .compileFile(schemaFile)
+      .withDistinguishedRootNode("e4", null)
+    val dp1 = pf.onPath("/")
+    val dp = reserializeDataProcessor(dp1)
+
+    val file = getResource("/test/sapi/myData16.dat")
+    val fis = new java.io.FileInputStream(file)
+    val input = new InputSourceDataInputStream(fis)
+    val outputter = new ScalaXMLInfosetOutputter()
+    val res = dp.parse(input, outputter)
+    val err = res.isError()
+    assertFalse(err)
+    assertEquals(5, res.location().bytePos1b())
+    assertEquals(33, res.location().bitPos1b())
+
+    val bos = new java.io.ByteArrayOutputStream()
+    val wbc = java.nio.channels.Channels.newChannel(bos)
+    val inputter = new ScalaXMLInfosetInputter(outputter.getResult)
+    val res2 = dp.unparse(inputter, wbc)
+    val err2 = res2.isError();
+    assertFalse(err2);
+    assertEquals("9100", bos.toString());
+  }
+
+  /**
+   * Verify that a user can get diagnostics without having to call isError
+   */
+  @Test
+  def testScalaAPIGetDiagnostics(): Unit = {
+    val c = Daffodil.compiler()
+
+    val schemaFile = new java.io.File("/test/sapi/notHere1.dfdl.xsd")
+    val pf = c.compileFile(schemaFile)
+    val diags = pf.getDiagnostics
+    val found1 = diags.exists { _.getMessage().contains("notHere1") }
+
+    assertTrue(found1)
+    assertTrue(pf.isError())
+  }
+
 }
diff --git 
a/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/processor/tdml/DaffodilTDMLDFDLProcessor.scala
 
b/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/processor/tdml/DaffodilTDMLDFDLProcessor.scala
index b1f933130..d1e3f7db2 100644
--- 
a/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/processor/tdml/DaffodilTDMLDFDLProcessor.scala
+++ 
b/daffodil-tdml-processor/src/main/scala/org/apache/daffodil/processor/tdml/DaffodilTDMLDFDLProcessor.scala
@@ -128,8 +128,8 @@ final class TDMLDFDLProcessorFactory private (
     optRootNamespace: Option[String],
   ): TDML.CompileResult = {
     val pf = compiler.compileSource(schemaSource, optRootName, 
optRootNamespace)
-    val diags = pf.getDiagnostics
     if (pf.isError) {
+      val diags = pf.getDiagnostics
       Left(diags)
     } else {
       val res = this.generateProcessor(pf, useSerializedProcessor)

Reply via email to