blob: 39d963be93db8b1e05d2ee1ef167993ce67961e7 [file] [log] [blame]
/*
* 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 actionContainers
import java.net.URI
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.SimpleFileVisitor
import java.nio.file.FileVisitResult
import java.nio.file.FileSystems
import java.nio.file.attribute.BasicFileAttributes
import java.nio.charset.StandardCharsets
import java.util.Base64
import javax.tools.ToolProvider
import collection.JavaConverters._
/**
* A collection of utility objects to create ephemeral action resources based
* on file contents.
*/
object ResourceHelpers {
/** Creates a zip file based on the contents of a top-level directory. */
object ZipBuilder {
def mkBase64Zip(sources: Seq[(Seq[String], String)]): String = {
val (tmpDir, _) = writeSourcesToTempDirectory(sources)
val archive = makeZipFromDir(tmpDir)
readAsBase64(archive)
}
}
/**
* A convenience object to compile and package Java sources into a JAR, and to
* encode that JAR as a base 64 string. The compilation options include the
* current classpath, which is why Google GSON is readily available (though not
* packaged in the JAR).
*/
object JarBuilder {
def mkBase64Jar(sources: Seq[(Seq[String], String)]): String = {
// Note that this pipeline doesn't delete any of the temporary files.
val binDir = compile(sources)
val jarPath = makeJarFromDir(binDir)
val base64 = readAsBase64(jarPath)
base64
}
def mkBase64Jar(source: (Seq[String], String)): String = {
mkBase64Jar(Seq(source))
}
private def compile(sources: Seq[(Seq[String], String)]): Path = {
require(!sources.isEmpty)
// The absolute paths of the source file
val (srcDir, srcAbsPaths) = writeSourcesToTempDirectory(sources)
// A temporary directory for the destination files.
val binDir = Files.createTempDirectory("bin").toAbsolutePath()
// Preparing the compiler
val compiler = ToolProvider.getSystemJavaCompiler()
val fileManager = compiler.getStandardFileManager(null, null, StandardCharsets.UTF_8)
// Collecting all files to be compiled
val compUnit = fileManager.getJavaFileObjectsFromFiles(srcAbsPaths.map(_.toFile).asJava)
// Setting the options
val compOptions = Seq(
"-d", binDir.toAbsolutePath().toString(),
"-classpath", buildClassPath())
val compTask = compiler.getTask(null, fileManager, null, compOptions.asJava, null, compUnit)
// ...and off we go.
compTask.call()
binDir
}
private def buildClassPath(): String = {
val bcp = System.getProperty("java.class.path")
val list = this.getClass().getClassLoader() match {
case ucl: URLClassLoader =>
bcp :: ucl.getURLs().map(_.getFile().toString()).toList
case _ =>
List(bcp)
}
list.mkString(System.getProperty("path.separator"))
}
}
/**
* Creates a temporary directory and reproduces the desired file structure
* in it. Returns the path of the temporary directory and the path of each
* file as represented in it.
*/
private def writeSourcesToTempDirectory(sources: Seq[(Seq[String], String)]): (Path, Seq[Path]) = {
// A temporary directory for the source files.
val srcDir = Files.createTempDirectory("src").toAbsolutePath()
val srcAbsPaths = for ((sourceName, sourceContent) <- sources) yield {
// The relative path of the source file
val srcRelPath = Paths.get(sourceName.head, sourceName.tail: _*)
// The absolute path of the source file
val srcAbsPath = srcDir.resolve(srcRelPath)
// Create parent directories if needed.
Files.createDirectories(srcAbsPath.getParent)
// Writing contents
Files.write(srcAbsPath, sourceContent.getBytes(StandardCharsets.UTF_8))
srcAbsPath
}
(srcDir, srcAbsPaths)
}
private def makeZipFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".zip")
private def makeJarFromDir(dir: Path): Path = makeArchiveFromDir(dir, ".jar")
/**
* Compresses all files beyond a directory into a zip file.
* Note that Jar files are just zip files.
*/
private def makeArchiveFromDir(dir: Path, extension: String): Path = {
// Any temporary file name for the archive.
val arPath = Files.createTempFile("output", extension).toAbsolutePath()
// We "mount" it as a filesystem, so we can just copy files into it.
val dstUri = new URI("jar:" + arPath.toUri().getScheme(), arPath.toAbsolutePath().toString(), null)
// OK, that's a hack. Doing this because newFileSystem wants to create that file.
arPath.toFile().delete()
val fs = FileSystems.newFileSystem(dstUri, Map(("create" -> "true")).asJava)
// Traversing all files in the bin directory...
Files.walkFileTree(dir, new SimpleFileVisitor[Path]() {
override def visitFile(path: Path, attributes: BasicFileAttributes) = {
// The path relative to the src dir
val relPath = dir.relativize(path)
// The corresponding path in the zip
val arRelPath = fs.getPath(relPath.toString())
// If this file is not top-level in the src dir...
if (relPath.getParent() != null) {
// ...create the directory structure if it doesn't exist.
if (!Files.exists(arRelPath.getParent())) {
Files.createDirectories(arRelPath.getParent())
}
}
// Finally we can copy that file.
Files.copy(path, arRelPath)
FileVisitResult.CONTINUE
}
})
fs.close()
arPath
}
/** Reads the contents of a (possibly binary) file into a base64-encoded String */
def readAsBase64(path: Path): String = {
val encoder = Base64.getEncoder()
new String(encoder.encode(Files.readAllBytes(path)), StandardCharsets.UTF_8)
}
}