stevedlawrence commented on code in PR #183:
URL: https://github.com/apache/daffodil-sbt/pull/183#discussion_r3222464679


##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +858,198 @@ object DaffodilPlugin extends AutoPlugin {
     }
   }
 
+  def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+    daffodilFlattenTarget := target.value / 
s"${name.value}-${version.value}-flat.zip",
+    /* Paths in daffodilFlattenExcludes/Includes that are not globbed at the 
start are
+     * generally only going to match paths within JAR files on the classpath.
+     * In order to deal with paths on the filesystem we need to glob the start
+     * of the path to account for different directory structure before the
+     * schema project path.
+     */
+    daffodilFlattenIncludes := "*.xsd" | "*.xsl" | "*.xslt" | "*.xml",
+    daffodilFlattenExcludes := HiddenFileFilter,
+
+    /**
+     * Whether or not to publish the flattened schemas zip. Defaults to false.
+     *
+     * If projects want to publish flattened schemas then they must explicitly 
enable it by
+     * setting 'daffodilFlattenSchemas / publishArtifact := true'.
+     *
+     * If false, flattened schemas will not be created unless you explicitly 
run the
+     * daffodilFlattenSchemas.
+     */
+    daffodilFlattenSchemas / publishArtifact := false,
+
+    daffodilFlattenSchemas / artifact := Artifact(
+      name.value,
+      "flat",
+      "zip",
+      Some("flat"),
+      Vector(),
+      None
+    ),
+    daffodilFlattenSchemas := {
+
+      val logger = streams.value.log
+      val filter = daffodilFlattenIncludes.value -- 
daffodilFlattenExcludes.value
+
+      val flatDir = target.value / "flatDir"
+      if (flatDir.exists())
+        IO.delete(flatDir)
+      IO.createDirectory(flatDir)
+
+      val projectResources = (Compile / resourceDirectories).value
+        .map { root => root.toURI.toURL -> (root ** 
filter).get.map(_.toURI.toURL).toList }
+        .toMap
+
+      /* Get all dependency jars and resources specific to this project. Note
+       * that the "Test" configuration is used for JAR files in order to ensure
+       * we pull in XSD files from daffodil-lib, as in many schema projects the
+       * daffodil dependencies are only used for testing, not compiling. Also
+       * note that we want to use Test/externalDependencyClasspath to get
+       * dependency jars, and not something like Test/fullClasspath or
+       * Test/dependencyClasspath, since those could trigger expensive resource
+       * generators or compilation of internal test jars that we don't need--we
+       * only need Compile/resources from this project and Test/dependency jars
+       * from external projects
+       */
+      val projectURLs = (Compile / 
resourceDirectories).value.map(_.toURI.toURL)
+      val allClasspathURLs = (Compile / 
fullClasspath).value.map(_.data.toURI.toURL)
+      val classLoader = new URLClassLoader((projectURLs ++ 
allClasspathURLs).toArray, null)
+
+      val schemaLocationPattern = """schemaLocation=\"([^\"]*)\"""".r
+      val hrefPattern = "href=\"([^\"]*)\"".r
+      val documentPattern = "document[(]'([^']*)'[)]".r
+      val referenceRegexes = List(schemaLocationPattern, hrefPattern, 
documentPattern)
+
+      val referencedFiles = {
+        // This annotation simply warns if the compiler cannot enable tail 
call optimization
+        @scala.annotation.tailrec
+        def getReferences(root: URL, parents: Seq[URL], acc: 
ArrayBuffer[URL]): Seq[URL] = {
+          val rootPath = Paths.get(root.toURI)
+          parents match {
+            // Already processed this file (h), proceed with rest of files (t)
+            case h :: t if (acc.contains(h)) =>
+              getReferences(
+                root,
+                t,
+                acc
+              )
+            // Need to process this file (h)
+            case h :: t => {
+              val relPath = h.toString match {
+                case j if (j.startsWith("jar:")) => 
Paths.get(j.toString.split("!")(1).tail)
+                case f if (f.startsWith("file:")) => 
rootPath.relativize(Paths.get(h.toURI))
+              }
+              val bytes = h.openStream().readAllBytes()
+              val newPath = Paths.get(
+                flatDir.toString,
+                relPath.toString.replaceAll("/", "__")
+              )
+              val bw = Files.newBufferedWriter(newPath)
+              val fileAsString = new String(bytes, Charset.defaultCharset())
+              val references = {
+                referenceRegexes.flatMap { re =>
+                  re.findAllIn(fileAsString).matchData.map(_.group(1))
+                }
+              }
+
+              // Resolve the location of all references on the actual file
+              // system
+              val resolvedReferences = references
+                .map { ref =>
+                  val origLocation = {
+                    if (ref.contains("urn:")) {
+                      val urn = ref.split(" ")(1)
+                      if (urn.startsWith("/"))
+                        urn.tail
+                      else
+                        urn
+                    } else if (ref.startsWith("/"))
+                      ref.tail
+                    else
+                      ref
+                  }
+                  val res = {
+                    val r = classLoader.findResource(origLocation)
+                    if (r == null) {
+                      val rrel = List(relPath.getParent, 
origLocation).mkString("/")
+                      classLoader.findResource(rrel)

Review Comment:
   As an example of my first paragraph, say we have resolved some include 
statement to the jar url:
   
   ```
   jar:///some/schema/format.jar!/com/example/foo.dfdl.xsd
   ```
   And say that foo.dfdl.xsd has a relative include like this:
   ```xml
   <include schemaLocation="bar.dfdl.xsd" />
   ```
   In this case, because the schemaLocation is a relative path Daffodil 
resolves bar.dfdl.xsd to a file inside format.jar, and it should resolve to 
this:
   ```
   jar:///some/schema/format.jar!/com/example/bar.dfdl.xsd
   ```
   
   What this logic does is it first resolves bar.dfdl.xsd path relative to the 
root `/com/example/foo.dfdl.xsd` to get `/com/example/bar.dfdl.xsd` and then 
looks for that on the classpath. Although this works most of the time, in 
theory it is possible for the resource `/com/example/bar/dfdl.xsd` to exist in 
multiple jars, and instead of finding the one in format.jar we might find one 
in some other jar if it appears earlier on the classpath, and so it would 
resolve to a file in a different jar.
   
   So if a path is relative, instead of building a resource path and looking 
for it on the classpath, we should instead resolve the path relative to the 
importing file. We really should only ever resolve via the class loader if the 
schemaLocation starts with a slash and so is absolute.
   
   Note that Daffodil does have deprecated behavior where it if it doesn't find 
a relative schemaLocation using the above logic then it tries to resolve the 
relative path as it it were absolute (i.e. using the classpath), so we might 
want that as fall back to match that behavior. So I think the logic wants to be 
something like:
   
   ```scala
   if (origLocation.startsWith("/")) {
      // absolute path, find it using the class loader with the slash removed
     classLoader.findResource(origLocation.tail) 
   } else {
      // relative path
   
     // first try to find it relative to the importing file without looking at 
the classpath
     // may need different logic depending on if importingFile is a jar:// or a 
file:// 
     val res = origLocation.resolvePathRelativeTo(importingFile) 
   
     if (res != null) {
       res
     } else {
       // if that failed, try the deprecated behavior of resolving relative 
paths as if they were absolute
       classLoader.findResource(origLocation)
     }
   }
   ```
   Here is where daffodil implements this logic, though it's a bit more complex 
since it also supports edge cases that I don't think this plugin needs to 
support:
   
   
https://github.com/apache/daffodil/blob/main/daffodil-core/src/main/scala/org/apache/daffodil/lib/xml/XMLUtils.scala#L1518-L1579
   
   ----
   Somewhat related, this made me think of another potential issue: In theory 
two separate jars could contain the same path and we could resolve to both of 
them separately from different files, but they would flatten to the same thing. 
For example, you could have something like
   
   ```
   jar:///header.jar!/header.dfdl.xsd
   jar:///header.jar!/common.dfdl.xsd
   jar:///payload.jar!/payload.dfdl.xsd
   jar:///payload.jar!/common.dfdl.xsd
   ```
   Where header.dfdl.xsd references the common.dfdl.xsd in header.jar, and 
payload.dfdl.xsd references common.dfdl.xsd in payload.jar. Although this 
should be avoided, it wouldn't surprise me if we actually have some schemas 
that do something like this, especilaly since we have lotsof schemas that are 
glue schemas that combine things like headers and payloads together. And in 
this case we would flatten to a single common.dfdl.xsd file even if the files 
were different and one would probably just overwrite the other?
   
   I wonder if the solution for the flattener is if a file is in a jar then we 
also include the jar name? So for example, the files above files flatten to:
   
   ```
   header__header.dfdl.xsd
   header__common.dfdl.xsd
   payload__payload.dfdl.xsd
   payload__common.dfdl.xsd 
   ```
   This still could break if jar files with the same name on the classpath, but 
that's probably alot less likely.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to