|
| 1 | +/* |
| 2 | + * Licensed to the Apache Software Foundation (ASF) under one or more |
| 3 | + * contributor license agreements. See the NOTICE file distributed with |
| 4 | + * this work for additional information regarding copyright ownership. |
| 5 | + * The ASF licenses this file to You under the Apache License, Version 2.0 |
| 6 | + * (the "License"); you may not use this file except in compliance with |
| 7 | + * the License. You may obtain a copy of the License at |
| 8 | + * |
| 9 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | + * |
| 11 | + * Unless required by applicable law or agreed to in writing, software |
| 12 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | + * See the License for the specific language governing permissions and |
| 15 | + * limitations under the License. |
| 16 | + */ |
| 17 | + |
| 18 | +package org.apache.spark.deploy |
| 19 | + |
| 20 | +import java.io.{File, FileInputStream, FileOutputStream} |
| 21 | +import java.nio.file.{Files, Path} |
| 22 | +import java.util.jar.{JarEntry, JarOutputStream} |
| 23 | +import javax.tools.{JavaFileObject, SimpleJavaFileObject, StandardLocation, ToolProvider} |
| 24 | + |
| 25 | +import com.google.common.io.ByteStreams |
| 26 | + |
| 27 | +import org.apache.spark.deploy.SparkSubmitUtils.MavenCoordinate |
| 28 | + |
| 29 | +import scala.collection.JavaConversions._ |
| 30 | + |
| 31 | +private[deploy] object IvyTestUtils { |
| 32 | + |
| 33 | + /** |
| 34 | + * Create the path for the jar and pom from the maven coordinate. Extension should be `jar` |
| 35 | + * or `pom`. |
| 36 | + */ |
| 37 | + private def pathFromCoordinate( |
| 38 | + artifact: MavenCoordinate, |
| 39 | + prefix: Path, |
| 40 | + ext: String, |
| 41 | + useIvyLayout: Boolean): Path = { |
| 42 | + val groupDirs = artifact.groupId.replace(".", File.separator) |
| 43 | + val artifactDirs = artifact.artifactId.replace(".", File.separator) |
| 44 | + val artifactPath = |
| 45 | + if (!useIvyLayout) { |
| 46 | + Seq(groupDirs, artifactDirs, artifact.version).mkString(File.separator) |
| 47 | + } else { |
| 48 | + Seq(groupDirs, artifactDirs, artifact.version, ext + "s").mkString(File.separator) |
| 49 | + } |
| 50 | + new File(prefix.toFile, artifactPath).toPath |
| 51 | + } |
| 52 | + |
| 53 | + private def artifactName(artifact: MavenCoordinate, ext: String = ".jar"): String = { |
| 54 | + s"${artifact.artifactId}-${artifact.version}$ext" |
| 55 | + } |
| 56 | + |
| 57 | + /** Write the contents to a file to the supplied directory. */ |
| 58 | + private def writeFile(dir: File, fileName: String, contents: String): File = { |
| 59 | + val outputFile = new File(dir, fileName) |
| 60 | + val outputStream = new FileOutputStream(outputFile) |
| 61 | + outputStream.write(contents.toCharArray.map(_.toByte)) |
| 62 | + outputStream.close() |
| 63 | + outputFile |
| 64 | + } |
| 65 | + |
| 66 | + /** Create an example Python file. */ |
| 67 | + private def createPythonFile(dir: File): File = { |
| 68 | + val contents = |
| 69 | + """def myfunc(x): |
| 70 | + | return x + 1 |
| 71 | + """.stripMargin |
| 72 | + writeFile(dir, "mylib.py", contents) |
| 73 | + } |
| 74 | + |
| 75 | + /** Compilable Java Source File */ |
| 76 | + private class JavaSourceFromString(val file: File, val code: String) |
| 77 | + extends SimpleJavaFileObject(file.toURI, JavaFileObject.Kind.SOURCE) { |
| 78 | + override def getCharContent(ignoreEncodingErrors: Boolean) = code |
| 79 | + } |
| 80 | + |
| 81 | + /** Create a simple testable Class. */ |
| 82 | + private def createJavaClass(dir: File, className: String, packageName: String): File = { |
| 83 | + val contents = |
| 84 | + s"""package $packageName; |
| 85 | + | |
| 86 | + |import java.lang.Integer; |
| 87 | + | |
| 88 | + |class $className implements java.io.Serializable { |
| 89 | + | |
| 90 | + | public $className() {} |
| 91 | + | |
| 92 | + | public Integer myFunc(Integer x) { |
| 93 | + | return x + 1; |
| 94 | + | } |
| 95 | + |} |
| 96 | + """.stripMargin |
| 97 | + val sourceFiles = Seq(new JavaSourceFromString(new File(dir, className + ".java"), contents)) |
| 98 | + val compiler = ToolProvider.getSystemJavaCompiler |
| 99 | + val fileManager = compiler.getStandardFileManager(null, null, null) |
| 100 | + |
| 101 | + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List(dir)) |
| 102 | + |
| 103 | + compiler.getTask(null, fileManager, null, null, null, sourceFiles).call() |
| 104 | + |
| 105 | + val fileName = s"${packageName.replace(".", File.separator)}${File.separator}$className.class" |
| 106 | + val result = new File(dir, fileName) |
| 107 | + assert(result.exists(), "Compiled file not found: " + result.getAbsolutePath) |
| 108 | + result |
| 109 | + } |
| 110 | + |
| 111 | + /** Helper method to write artifact information in the pom. */ |
| 112 | + private def pomArtifactWriter(artifact: MavenCoordinate, tabCount: Int = 1): String = { |
| 113 | + var result = "\n" + " " * tabCount + s"<groupId>${artifact.groupId}</groupId>" |
| 114 | + result += "\n" + " " * tabCount + s"<artifactId>${artifact.artifactId}</artifactId>" |
| 115 | + result += "\n" + " " * tabCount + s"<version>${artifact.version}</version>" |
| 116 | + result |
| 117 | + } |
| 118 | + |
| 119 | + /** Create a pom file for this artifact. */ |
| 120 | + private def createPom( |
| 121 | + dir: File, |
| 122 | + artifact: MavenCoordinate, |
| 123 | + dependencies: Option[Seq[MavenCoordinate]]): File = { |
| 124 | + var content = """ |
| 125 | + |<?xml version="1.0" encoding="UTF-8"?> |
| 126 | + |<project xmlns="http://maven.apache.org/POM/4.0.0" |
| 127 | + | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 128 | + | xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 |
| 129 | + | http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| 130 | + | <modelVersion>4.0.0</modelVersion> |
| 131 | + """.stripMargin.trim |
| 132 | + content += pomArtifactWriter(artifact) |
| 133 | + content += dependencies.map { deps => |
| 134 | + val inside = deps.map { dep => |
| 135 | + "\t<dependency>" + pomArtifactWriter(dep, 3) + "\n\t</dependency>" |
| 136 | + }.mkString("\n") |
| 137 | + "\n <dependencies>\n" + inside + "\n </dependencies>" |
| 138 | + }.getOrElse("") |
| 139 | + content += "\n</project>" |
| 140 | + writeFile(dir, artifactName(artifact, ".pom"), content.trim) |
| 141 | + } |
| 142 | + |
| 143 | + /** Create the jar for the given maven coordinate, using the supplied files. */ |
| 144 | + private def packJar( |
| 145 | + dir: File, |
| 146 | + artifact: MavenCoordinate, |
| 147 | + files: Seq[(String, File)]): File = { |
| 148 | + val jarFile = new File(dir, artifactName(artifact)) |
| 149 | + val jarFileStream = new FileOutputStream(jarFile) |
| 150 | + val jarStream = new JarOutputStream(jarFileStream, new java.util.jar.Manifest()) |
| 151 | + |
| 152 | + for (file <- files) { |
| 153 | + val jarEntry = new JarEntry(file._1) |
| 154 | + jarStream.putNextEntry(jarEntry) |
| 155 | + |
| 156 | + val in = new FileInputStream(file._2) |
| 157 | + ByteStreams.copy(in, jarStream) |
| 158 | + in.close() |
| 159 | + } |
| 160 | + jarStream.close() |
| 161 | + jarFileStream.close() |
| 162 | + |
| 163 | + jarFile |
| 164 | + } |
| 165 | + |
| 166 | + /** |
| 167 | + * Creates a jar and pom file, mocking a Maven repository. The root path can be supplied with |
| 168 | + * `tempDir`, dependencies can be created into the same repo, and python files can also be packed |
| 169 | + * inside the jar. |
| 170 | + * |
| 171 | + * @param artifact The maven coordinate to generate the jar and pom for. |
| 172 | + * @param dependencies List of dependencies this artifact might have to also create jars and poms. |
| 173 | + * @param tempDir The root folder of the repository |
| 174 | + * @param useIvyLayout whether to mock the Ivy layout for local repository testing |
| 175 | + * @param withPython Whether to pack python files inside the jar for extensive testing. |
| 176 | + * @return |
| 177 | + */ |
| 178 | + private def createLocalRepository( |
| 179 | + artifact: MavenCoordinate, |
| 180 | + dependencies: Option[Seq[MavenCoordinate]] = None, |
| 181 | + tempDir: Option[Path] = None, |
| 182 | + useIvyLayout: Boolean = false, |
| 183 | + withPython: Boolean = false): Path = { |
| 184 | + // Where the root of the repository exists, and what Ivy will search in |
| 185 | + val tempPath = tempDir.getOrElse(Files.createTempDirectory(null)) |
| 186 | + // Where to temporary class files and such |
| 187 | + val root = Files.createTempDirectory(tempPath, null).toFile |
| 188 | + val artifactPath = pathFromCoordinate(artifact, tempPath) |
| 189 | + Files.createDirectories(artifactPath) |
| 190 | + val className = "MyLib" |
| 191 | + |
| 192 | + val javaClass = createJavaClass(root, className, artifact.groupId) |
| 193 | + // A tuple of files representation in the jar, and the file |
| 194 | + val javaFile = (artifact.groupId.replace(".", "/") + "/" + javaClass.getName, javaClass) |
| 195 | + val allFiles = |
| 196 | + if (withPython) { |
| 197 | + val pythonFile = createPythonFile(root) |
| 198 | + Seq(javaFile, (pythonFile.getName, pythonFile)) |
| 199 | + } else { |
| 200 | + Seq(javaFile) |
| 201 | + } |
| 202 | + val jarFile = packJar(artifactPath.toFile, artifact, allFiles) |
| 203 | + assert(jarFile.exists(), "Problem creating Jar file") |
| 204 | + val pomFile = createPom(artifactPath.toFile, artifact, dependencies) |
| 205 | + assert(pomFile.exists(), "Problem creating Pom file") |
| 206 | + tempPath |
| 207 | + } |
| 208 | + |
| 209 | + private[databricks] def createLocalRepositoryForTests( |
| 210 | + artifact: String, |
| 211 | + dependencies: Option[String], |
| 212 | + withPython: Boolean = false): Path = { |
| 213 | + val art = parseCoordinates(artifact).head |
| 214 | + val deps = dependencies.map(parseCoordinates) |
| 215 | + val mainRepo = createLocalRepository(art, deps, withPython) |
| 216 | + deps.foreach { seq => seq.foreach { dep => |
| 217 | + createLocalRepository(dep, None, false, Some(mainRepo)) |
| 218 | + }} |
| 219 | + |
| 220 | + mainRepo |
| 221 | + } |
| 222 | +} |
0 commit comments