Extract a Text File from included Jar Programatically with Gradle

Benjamin Hoogterp
3 min readMar 10, 2022

--

Synopsis

An included Jar is distributed as a public consumable. In its essence, it is really a .zip file, with a particular directory structure inside of it. Particularly, any files from the resources folder are placed in the root of the zip file path. What if we want to extract one of those text files which was packaged into the Jar as part of the package, but which we need to do some operations on or with? A suitable example of this is processing out a swagger api documentation.

Extracting the resource file from the Jar

At its essence, the Jar is simply a zip file, and you are looking to extract a known filename from the root path. This can be accomplished in a number of ways, but this specifically looks at using a custom Gradle task.

To do this, first, you need to mark which jars you wish to extract files from. Again, there are several approaches, but the simplest is to create a custom configuration and then import those dependencies using that configuration:

plugins {
id "java"
}
configurations {
implementation.extendsFrom pullResources
}
dependencies {
implementation "group:artifact:version"
// ... whatever normal dependencies you need
pullResources "com.example:artifact-with-resource-1:1.0.0"
pullResources "com.example:artifact-with-resource-2:1.0.0"
}

You simply define all the normal dependencies as you would normally (typically with “implementation”), and define the ones with resources you wish to extract with the new configuration (“pullResources”).

Now comes the custom task to extract a resource from that jar using a custom task:

task extractResourceFile() {
doLast {
def path = "openapi-spec.yml"
configurations.pullResources.getFiles()
.each {
def zip = new java.util.zip.ZipFile(it)
def entry = zip.getEntry(path)
if (entry == null) return

String contents = zip.getInputStream(entry).text
if (contents == null) return
// We start with the same filename in each of the jar files.
// We will need to add something to that filename so when
// we write these, they will have unique names. We chose
// to remove the version component of the filename and prepend
// that to the original filename.
String name = it.name
if (name.contains("-SNAPSHOT")) name = name.substring(0, name.lastIndexOf("-SNAPSHOT"))
name = name.substring(0, name.lastIndexOf("-"))

// Here you can modify the contents before writing the contents to a file...
contents = contents.replaceAll("basePath: ", "# basePath:")
// Finally, we write the contents to a file new File(sourcePath()).mkdirs()
String newFileName = "${buildDir}/$name-$path"
def newFile = new File(newFileName)
newFile.createNewFile()
newFile.text = contents
}
}
}

This will read a file named “openapi-spec.yml” from each jar imported with “pullDepenendencies”, and output them into the build folder as “<artifact>-openapi-spec.yml”.

Conclusion

This method would then allow operations on the resources of dependencies. Possibilities include using npm modules to merge, modify, or otherwise operate on them. There are obviously other ways to achieve the same result since the Jar is simply a zip file, but if your process relies heavily on Gradle for build automation already, this technique can allow you to keep a single-source-of-truth for resources coming from dependent jars.

--

--

No responses yet