stevedlawrence commented on code in PR #183:
URL: https://github.com/apache/daffodil-sbt/pull/183#discussion_r3234778482
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
Review Comment:
Indengation is incorrect. Can you re-run scalafmt?
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
Review Comment:
Should we avoid matching xsi:schemaLocation since those have a different
syntax? So it's something like:
```scala
"""(?<!xsi:)schemaLocation=\"([^\"]*)\"""".r
```
Then we don't need this string split stuff below:
```
val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
```
I don't know if there's anything where we actually use xsi:schemaLocation.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
+ flatDir.toString,
+ contextRelPath.toString.replaceAll("/", "__"))
+ val bw = Files.newBufferedWriter(contextFlatPath)
+ val fileAsString = new String(bytes, Charset.defaultCharset())
+
+ val references = referenceRegexes.flatMap { re =>
+ re.findAllIn(fileAsString).matchData.map(_.group(1))
+ }
+
+ val resolvedRefs = references.map { ref =>
+ val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
+ if (origLocation.startsWith("/")) {
+ // Dealing with an absolute path
+ val url = Option(classLoader.findResource(origLocation.tail))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve absolute reference to $ref
from source file $contextURI")
+ None
+ }
+ } else {
+ // Relative path
+ val relPath =
contextRelPath.resolveSibling(origLocation).normalize()
+ if (Files.exists(rootPath.resolve(relPath))) {
Review Comment:
This doesn't have the same logic as Daffodil. If contextURI is a
`jar:file:/..` and it contains a relative path, then this will look for the
relative path relative to `rootPath`. But instead it should look inside the
same jar as contextURI.
For example, if the contextURI is
```
jar:file:/path/to/foo.jar!/path/to/schema.dfdl.xsd
```
and that schema.dfdl.xsd references `schemaLocation="foo.dfdl.xsd"`, we
should be looking for the file at:
```
jar:file:/path/to/foo.jar!/path/to/foo.dfdl.xsd
```
But this logic will check in
```
resourceDirectoryRoot/path/to/foo.dfdl.xsd
```
If the context is a jar, we shouldn't even consider looking at the
filesystem.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
Review Comment:
If this is a jar we should prefix the jar name to the path. It doesn't hurt
anything and avoids potential issues where two jars have the same name.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
+ flatDir.toString,
+ contextRelPath.toString.replaceAll("/", "__"))
+ val bw = Files.newBufferedWriter(contextFlatPath)
+ val fileAsString = new String(bytes, Charset.defaultCharset())
+
+ val references = referenceRegexes.flatMap { re =>
+ re.findAllIn(fileAsString).matchData.map(_.group(1))
+ }
+
+ val resolvedRefs = references.map { ref =>
+ val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
+ if (origLocation.startsWith("/")) {
+ // Dealing with an absolute path
+ val url = Option(classLoader.findResource(origLocation.tail))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve absolute reference to $ref
from source file $contextURI")
+ None
+ }
+ } else {
+ // Relative path
+ val relPath =
contextRelPath.resolveSibling(origLocation).normalize()
+ if (Files.exists(rootPath.resolve(relPath))) {
+ // Found the file on the regular filesystem
+ Some(origLocation -> rootURI.resolve(relPath.toString))
+ } else {
+ // Check the classpath for the current reference relative to
the
+ // current contextURI
+ val url = Option(classLoader.findResource(relPath.toString))
Review Comment:
Same here, this isn't quite the same logic that Daffodil uses. I think this
is handling the jar case, but it's not looking at the same jar that contextURI
is in, it's just looking at the classpath and so could find something in a
different jar.
##########
README.md:
##########
@@ -323,6 +323,36 @@ root `src/` directory, and all test source and resource
files to be in a root
`test/` directory. Source files are those that end with `*.scala` or `*.java`,
and resource files are anything else.
+### Flatten Schemas
+
+This plugin has functionality to flatten the directory structure of 1 or more
+schema projects, renaming the schema files and upating schemaLocation's as
+necessary.
+
+```bash
+sbt daffodilFlattenSchemas
+```
+
+The renaming works as follows:
+
+`org/apache/daffodil/xsd/main.dfdl.xsd`
+
+will be renamed to:
+
+`org__apache__daffodil__xsd__main.dfdl.xsd`
+
+Note: Original files are not modified, they are simply copied to the specified
+output directory with the new name and updated schemaLocation's.
+
+Many non-Daffodil XML/XSD programs (such as XML validators) do not resolve
Review Comment:
Nice explanation for why this is needed, I might recommend moving it to the
top of this section so it acts as an introduction as to why this task exists
and why things are renamed the way they are. Otherwise the above logic feels
kindof arbitrary until you get to this paragraph.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
Review Comment:
This is an issue with the using root mentioned above, it's possible that the
contetxURI is a different root that rootURI because the are multple possible
roots for a URI--it isn't necessarily the same one as this root we're looking
at.
So I think this wants to be something like this:
```scala
case "file" => {
val root = (Compile /
resourceDirectories).value.find(contextURI.startsWith).get
Paths.get(root.relativize(contextURI))
}
```
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
+ flatDir.toString,
+ contextRelPath.toString.replaceAll("/", "__"))
+ val bw = Files.newBufferedWriter(contextFlatPath)
+ val fileAsString = new String(bytes, Charset.defaultCharset())
+
+ val references = referenceRegexes.flatMap { re =>
+ re.findAllIn(fileAsString).matchData.map(_.group(1))
+ }
+
+ val resolvedRefs = references.map { ref =>
+ val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
+ if (origLocation.startsWith("/")) {
+ // Dealing with an absolute path
+ val url = Option(classLoader.findResource(origLocation.tail))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve absolute reference to $ref
from source file $contextURI")
+ None
+ }
+ } else {
+ // Relative path
+ val relPath =
contextRelPath.resolveSibling(origLocation).normalize()
+ if (Files.exists(rootPath.resolve(relPath))) {
Review Comment:
In this review, I've thought of a new issue if the contextURI is a file://
and the origLocation is a relative path. In that case a relative resource path
could access a file in a different resourceDirectory since they are would be in
the same jar. So I think the logic for looking up relative paths when the
context is a file:// wants to be something like this:
```scala
val resolvedRoot = (Compile / resourceDirectories).value.find(root =>
File.exists(root.resolve(relPath)))
if (resolvedRoot.isDefined) {
Some(origLocation -> resolvedRoot.resolve(relPath))
}
```
So similar to what you have, but we have try to resolve the path relative to
all possible roots instead of the current root. Similar to above, that is
because a path relative to a file:// could resolve to any resource root
directory, not just the current one.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
+ flatDir.toString,
+ contextRelPath.toString.replaceAll("/", "__"))
+ val bw = Files.newBufferedWriter(contextFlatPath)
+ val fileAsString = new String(bytes, Charset.defaultCharset())
+
+ val references = referenceRegexes.flatMap { re =>
+ re.findAllIn(fileAsString).matchData.map(_.group(1))
+ }
+
+ val resolvedRefs = references.map { ref =>
+ val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
+ if (origLocation.startsWith("/")) {
+ // Dealing with an absolute path
+ val url = Option(classLoader.findResource(origLocation.tail))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve absolute reference to $ref
from source file $contextURI")
+ None
+ }
+ } else {
+ // Relative path
+ val relPath =
contextRelPath.resolveSibling(origLocation).normalize()
+ if (Files.exists(rootPath.resolve(relPath))) {
+ // Found the file on the regular filesystem
+ Some(origLocation -> rootURI.resolve(relPath.toString))
+ } else {
+ // Check the classpath for the current reference relative to
the
+ // current contextURI
+ val url = Option(classLoader.findResource(relPath.toString))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ // Maybe this is actually an absolute path not within the
same
+ // context. Check the classpath for the original location
+ val url2 = Option(classLoader.findResource(origLocation))
+ if (url2.isDefined)
+ Some(origLocation -> url2.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve local reference to $ref
from source file $contextURI")
+ None
+ }
+ }
+ }
+ }
+ }.flatten.toMap
+
+ val updatedFileAsString = {
+ resolvedRefs.foldLeft(fileAsString) {
+ case (input, (ref, resolvedRef)) => {
+ // For each reference replace each instance of it in the
+ // file with the same reference but with "/" changed to "__"
+ val relativized = resolvedRef.getScheme match {
+ case "jar" => resolvedRef.toString.split("!")(1).tail
+ case _ => rootURI.relativize(resolvedRef).toString
+ }
+ input.replaceAll(ref, relativized.replaceAll("/", "__"))
Review Comment:
Same as above, we should prefix the jar name if the schema is a jar. It
might be worth adding a function that converts a URI to a flat path, since we
need it in two places 1) above when creating the flat file 2) here when
creating a reference to a flat file. Maybe it's define at the top of this
setting or something since it's really only needed by this setting.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
Review Comment:
Our coding convention doesn't usually drop periods, e.g. this wants to be
`projectResources.foreach { ...
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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 }
Review Comment:
I think including the root in this doesn't really work in the following
logic. Just because a specific resource file is resolved from some root
directory it doesn't that the thigns that file references use that same root.
They could be in a different root or a jar, so carrying around the root I don't
think really works.
Ithink this just wants to be
```scala
val projectResources = (Compile / resourceDirectories).value ** filter
```
And the below logic needs to figure out which root directory files are in
and strip it off.
Note that this would mean we no longer have `projectResource.foreach { ...
}`, I think you just push all the project resources to the unprocessed stack
and then process them as normal. Later code in other comments then has to
figure out which root a file:// resource actually came frome.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
+ flatDir.toString,
+ contextRelPath.toString.replaceAll("/", "__"))
+ val bw = Files.newBufferedWriter(contextFlatPath)
+ val fileAsString = new String(bytes, Charset.defaultCharset())
+
+ val references = referenceRegexes.flatMap { re =>
+ re.findAllIn(fileAsString).matchData.map(_.group(1))
+ }
+
+ val resolvedRefs = references.map { ref =>
+ val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
+ if (origLocation.startsWith("/")) {
+ // Dealing with an absolute path
+ val url = Option(classLoader.findResource(origLocation.tail))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve absolute reference to $ref
from source file $contextURI")
+ None
+ }
+ } else {
+ // Relative path
+ val relPath =
contextRelPath.resolveSibling(origLocation).normalize()
+ if (Files.exists(rootPath.resolve(relPath))) {
+ // Found the file on the regular filesystem
+ Some(origLocation -> rootURI.resolve(relPath.toString))
+ } else {
+ // Check the classpath for the current reference relative to
the
+ // current contextURI
+ val url = Option(classLoader.findResource(relPath.toString))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ // Maybe this is actually an absolute path not within the
same
+ // context. Check the classpath for the original location
+ val url2 = Option(classLoader.findResource(origLocation))
+ if (url2.isDefined)
+ Some(origLocation -> url2.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve local reference to $ref
from source file $contextURI")
+ None
+ }
+ }
+ }
+ }
+ }.flatten.toMap
+
+ val updatedFileAsString = {
+ resolvedRefs.foldLeft(fileAsString) {
+ case (input, (ref, resolvedRef)) => {
+ // For each reference replace each instance of it in the
+ // file with the same reference but with "/" changed to "__"
+ val relativized = resolvedRef.getScheme match {
+ case "jar" => resolvedRef.toString.split("!")(1).tail
+ case _ => rootURI.relativize(resolvedRef).toString
+ }
+ input.replaceAll(ref, relativized.replaceAll("/", "__"))
+ }
+ }
+ }
+
+ bw.write(updatedFileAsString)
+ bw.close()
+ processed += contextURI
+ unprocessed.pushAll((resolvedRefs.values.toSet diff
processed).filterNot(unprocessed contains _))
Review Comment:
I feel like it's a bit hard to make sense of the diff + filterNot. I think
this logic is could be simplified if instead of keeping track of processed vs
unprocessed, we keep track of processed vs "seen", where "seen" contains URIs
that have either been processed or are in the unprocessed stack. So it kindof
has this logic you have here already built-in to it. Note that this "seen"
logic is similar to the logic Daffodil uses when it resolves includes/imports
so it's kindof consistent.
So this logic becomes something like:
```scala
// add all resolved references to the seen Set. Any references we haven't
already seen
// must never have been referenced before and so we add them to the stack to
be processed
val unseen = resolvedReferences.filter(seen.add)
unprocessed.pushAll(unseen)
```
The only additional change needed with this approach is when we initialize
the unprocessed stack/queue we must also add those to the seen set, because
those are in the unprocessed queue and so will eventually be processed even if
something else references them.
This way when we see a reference, we don't ever have to check if it wasn't
already processed AND isn't already in the unprocessed stack. We just mark it
as seen, and if it's the first time seeing it we add it the to list to be
processed.
##########
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)
+ } else
+ r
+ }
+ if (res == null)
+ logger.warn(s"Unable to resolve reference to $ref from
source file $h")
+ ref -> Option(res)
+ }
+ .toMap
+ .filter(e => e._2.isDefined)
+
+ // For each reference do a search and replace of the entire file
+ val updatedFileAsString = {
+ resolvedReferences.foldLeft(fileAsString) {
+ case (input, (ref, resolvedRef)) => {
+ // For each reference replace each instance of it in the
+ // file with the same reference but with "/" changed to
"__"
+ val rref = resolvedRef.get.toString
+ val relativized = rref match {
+ case j if (j.startsWith("jar")) => j.split("!")(1).tail
+ case f if (f.startsWith("file")) =>
rootPath.relativize(Paths.get(resolvedRef.get.toURI)).toString
+ }
+ input.replaceAll(ref, relativized.replaceAll("/", "__"))
+ }
+ }
+ }
+
+ bw.write(updatedFileAsString)
+ bw.close()
+
+ /* Have succesfully processed this file (h), call getReferences
+ * again on the rest of the list (t) + any references from this
+ * file. Add this file to the accumulator list
+ */
+ getReferences(
+ root,
Review Comment:
I believe some projects do exist that make use of resource generators. We
also don't know what other project might exists. They probably aren't common,
but they could exists. And in those cases we will have separate resource
directories and those files could reference each other. I feel it's important
to make sure this works correctly and I don't think adds too much complexity.
My latest review adds some comments about how we might be able to do this.
##########
src/main/scala/org/apache/daffodil/DaffodilPlugin.scala:
##########
@@ -840,6 +854,179 @@ object DaffodilPlugin extends AutoPlugin {
}
}
+ def daffodilFlattenSettings: Seq[Setting[_]] = Seq(
+ daffodilFlattenTarget := target.value /
s"${name.value}-${version.value}-flat.zip",
+
+ /**
+ * 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
+ ),
+
+ /**
+ * Only grab the file types that need to be flattened. These files are only
+ * grabbed from the root project. Any referenced files will be pulled in
+ * from the classpath. Note that XML files are commonly used in XSLT for
+ * settings.
+ */
+ daffodilFlattenSchemas / includeFilter := "*.xsd" | "*.xsl" | "*.xslt" |
"*.xml",
+ daffodilFlattenSchemas / excludeFilter := HiddenFileFilter,
+
+ daffodilFlattenResourceReferencePatterns := List(
+ """schemaLocation=\"([^\"]*)\"""".r,
+ "href=\"([^\"]*)\"".r,
+ "document[(]'([^']*)'[)]".r),
+
+ daffodilFlattenSchemas / products := {
+
+ val logger = streams.value.log
+ val filter = (daffodilFlattenSchemas / includeFilter).value --
(daffodilFlattenSchemas / excludeFilter).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
+
+ /**
+ * Create a URLClassLoader object with URLs to all resources used by the
+ * project. The class loader will be used to resolve references made
+ * within the flattened files.
+ */
+ val projectURLs = (Compile /
resourceDirectories).value.map(_.toURI.toURL)
+ val allClasspathURLs = (Test /
externalDependencyClasspath).value.map(_.data.toURI.toURL)
+ val classLoader = new URLClassLoader((projectURLs ++
allClasspathURLs).toArray, null)
+
+ val referenceRegexes = daffodilFlattenResourceReferencePatterns.value
+
+ val processed = scala.collection.mutable.Set[URI]()
+ projectResources foreach { case (rootURL, urls) =>
+ val unprocessed = scala.collection.mutable.Stack[URI]()
+ val rootURI = rootURL.toURI
+ val rootPath = Paths.get(rootURI)
+ unprocessed.pushAll(urls.map(_.toURI))
+ while (!unprocessed.isEmpty) {
+ val contextURI = unprocessed.pop
+ val bytes = contextURI.toURL.openStream().readAllBytes()
+ val contextRelPath = contextURI.getScheme match {
+ case "jar" => Paths.get(contextURI.toString.split("!")(1).tail)
+ case "file" => Paths.get(rootURI.relativize(contextURI).getPath)
+ case _ => throw new IllegalArgumentException(s"Unrecognized URI
scheme: $contextURI")
+ }
+ val contextFlatPath = Paths.get(
+ flatDir.toString,
+ contextRelPath.toString.replaceAll("/", "__"))
+ val bw = Files.newBufferedWriter(contextFlatPath)
+ val fileAsString = new String(bytes, Charset.defaultCharset())
+
+ val references = referenceRegexes.flatMap { re =>
+ re.findAllIn(fileAsString).matchData.map(_.group(1))
+ }
+
+ val resolvedRefs = references.map { ref =>
+ val origLocation = if (ref contains " ") ref.split(" ")(1) else ref
+ if (origLocation.startsWith("/")) {
+ // Dealing with an absolute path
+ val url = Option(classLoader.findResource(origLocation.tail))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve absolute reference to $ref
from source file $contextURI")
+ None
+ }
+ } else {
+ // Relative path
+ val relPath =
contextRelPath.resolveSibling(origLocation).normalize()
+ if (Files.exists(rootPath.resolve(relPath))) {
+ // Found the file on the regular filesystem
+ Some(origLocation -> rootURI.resolve(relPath.toString))
+ } else {
+ // Check the classpath for the current reference relative to
the
+ // current contextURI
+ val url = Option(classLoader.findResource(relPath.toString))
+ if (url.isDefined)
+ Some(origLocation -> url.get.toURI)
+ else {
+ // Maybe this is actually an absolute path not within the
same
+ // context. Check the classpath for the original location
+ val url2 = Option(classLoader.findResource(origLocation))
+ if (url2.isDefined)
+ Some(origLocation -> url2.get.toURI)
+ else {
+ logger.warn(s"Unable to resolve local reference to $ref
from source file $contextURI")
+ None
+ }
+ }
+ }
+ }
+ }.flatten.toMap
+
+ val updatedFileAsString = {
+ resolvedRefs.foldLeft(fileAsString) {
+ case (input, (ref, resolvedRef)) => {
+ // For each reference replace each instance of it in the
+ // file with the same reference but with "/" changed to "__"
+ val relativized = resolvedRef.getScheme match {
+ case "jar" => resolvedRef.toString.split("!")(1).tail
+ case _ => rootURI.relativize(resolvedRef).toString
+ }
+ input.replaceAll(ref, relativized.replaceAll("/", "__"))
+ }
+ }
+ }
+
+ bw.write(updatedFileAsString)
+ bw.close()
+ processed += contextURI
+ unprocessed.pushAll((resolvedRefs.values.toSet diff
processed).filterNot(unprocessed contains _))
+ }
+ }
+
+ /* Create zip file containing all flattened schemas */
+ val flattenedFiles = IO.listFiles(flatDir)
Review Comment:
Another though about reproducibility, can we sort the flattned files by name
so that the order is consistent. Just another way to possibly improve
reproducibilty.
--
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]