a8c630387001d6311812b1d982555dc19b7eee9f
[ccsdk/cds.git] /
1 /*
2  * Copyright © 2019 IBM.
3  *
4  *  Licensed under the Apache License, Version 2.0 (the "License");
5  *  you may not use this file except in compliance with the License.
6  *  You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  */
16
17 package org.onap.ccsdk.cds.controllerblueprints.core.scripts
18
19 import kotlinx.coroutines.async
20 import kotlinx.coroutines.coroutineScope
21 import org.jetbrains.kotlin.cli.common.ExitCode
22 import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments
23 import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation
24 import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
25 import org.jetbrains.kotlin.cli.common.messages.MessageCollector
26 import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
27 import org.jetbrains.kotlin.config.Services
28 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintConstants
29 import org.onap.ccsdk.cds.controllerblueprints.core.BluePrintException
30 import org.onap.ccsdk.cds.controllerblueprints.core.checkFileExists
31 import org.onap.ccsdk.cds.controllerblueprints.core.logger
32 import org.onap.ccsdk.cds.controllerblueprints.core.utils.BluePrintFileUtils
33 import java.io.File
34 import java.net.URLClassLoader
35 import java.util.ArrayList
36 import kotlin.script.experimental.api.SourceCode
37 import kotlin.script.experimental.jvm.util.classpathFromClasspathProperty
38 import kotlin.system.measureTimeMillis
39
40 open class BluePrintCompileService {
41     val log = logger(BluePrintCompileService::class)
42
43     companion object {
44         val classPaths = classpathFromClasspathProperty()?.joinToString(File.pathSeparator) {
45             it.absolutePath
46         }
47     }
48
49     /** Compile the [bluePrintSourceCode] and get the [kClassName] instance for the constructor [args] */
50     suspend fun <T> eval(
51         bluePrintSourceCode: BluePrintSourceCode,
52         kClassName: String,
53         args: ArrayList<Any?>?
54     ): T {
55         /** Compile the source code if needed */
56         log.debug("Jar Exists : ${bluePrintSourceCode.targetJarFile.exists()}, Regenerate : ${bluePrintSourceCode.regenerate}")
57         if (!bluePrintSourceCode.targetJarFile.exists() || bluePrintSourceCode.regenerate) {
58             compile(bluePrintSourceCode)
59         }
60
61         val classLoaderWithDependencies = if (BluePrintConstants.USE_SCRIPT_COMPILE_CACHE) {
62             /** Get the class loader with compiled jar from cache */
63             BluePrintCompileCache.classLoader(bluePrintSourceCode.cacheKey)
64         } else {
65             /** Get the class loader with compiled jar from disk */
66             BluePrintFileUtils.getURLClassLoaderFromDirectory(bluePrintSourceCode.cacheKey)
67         }
68
69         /** Create the instance from the class loader */
70         return instance(classLoaderWithDependencies, kClassName, args)
71     }
72
73     /** Compile [bluePrintSourceCode] and put into cache */
74     suspend fun compile(bluePrintSourceCode: BluePrintSourceCode) {
75         // TODO("Include Multiple folders")
76         val sourcePath = bluePrintSourceCode.blueprintKotlinSources.first()
77         val compiledJarFile = bluePrintSourceCode.targetJarFile
78
79         log.info("compiling for cache key(${bluePrintSourceCode.cacheKey})")
80         coroutineScope {
81             val timeTaken = measureTimeMillis {
82                 /** Create compile arguments */
83                 val args = mutableListOf<String>().apply {
84                     add("-no-stdlib")
85                     add("-no-reflect")
86                     add("-module-name")
87                     add(bluePrintSourceCode.moduleName)
88                     add("-cp")
89                     add(classPaths!!)
90                     add(sourcePath)
91                     add("-d")
92                     add(compiledJarFile.absolutePath)
93                 }
94                 val deferredCompile = async {
95                     val k2jvmCompiler = K2JVMCompiler()
96                     /** Construct Arguments */
97                     val arguments = k2jvmCompiler.createArguments()
98                     parseCommandLineArguments(args, arguments)
99                     val messageCollector = CompilationMessageCollector()
100                     /** Compile with arguments */
101                     val exitCode: ExitCode = k2jvmCompiler.exec(messageCollector, Services.EMPTY, arguments)
102                     when (exitCode) {
103                         ExitCode.OK -> {
104                             checkFileExists(compiledJarFile) { "couldn't generate compiled jar(${compiledJarFile.absolutePath})" }
105                         }
106                         else -> {
107                             throw BluePrintException("$exitCode :${messageCollector.errors().joinToString("\n")}")
108                         }
109                     }
110                 }
111                 deferredCompile.await()
112             }
113             log.info("compiled in ($timeTaken)mSec for cache key(${bluePrintSourceCode.cacheKey})")
114         }
115     }
116
117     /** create class [kClassName] instance from [classLoader] */
118     fun <T> instance(classLoader: URLClassLoader, kClassName: String, args: ArrayList<Any?>? = arrayListOf()): T {
119         val kClazz = classLoader.loadClass(kClassName)
120             ?: throw BluePrintException("failed to load class($kClassName) from current class loader.")
121
122         val instance = if (args.isNullOrEmpty()) {
123             kClazz.newInstance()
124         } else {
125             kClazz.constructors
126                 .single().newInstance(*args.toArray())
127         } ?: throw BluePrintException("failed to create class($kClassName) instance for constructor argument($args).")
128
129         if (!BluePrintConstants.USE_SCRIPT_COMPILE_CACHE) {
130             classLoader.close()
131         }
132         return instance as T
133     }
134 }
135
136 /** Compile source code information */
137 open class BluePrintSourceCode : SourceCode {
138
139     lateinit var blueprintKotlinSources: MutableList<String>
140     lateinit var moduleName: String
141     lateinit var targetJarFile: File
142     lateinit var cacheKey: String
143     var regenerate: Boolean = false
144
145     override val text: String
146         get() = ""
147
148     override val locationId: String? = null
149
150     override val name: String?
151         get() = moduleName
152 }
153
154 /** Class to collect compilation Data */
155 data class CompiledMessageData(
156     val severity: CompilerMessageSeverity,
157     val message: String,
158     val location: CompilerMessageLocation?
159 )
160
161 /** Class to collect compilation results */
162 class CompilationMessageCollector : MessageCollector {
163
164     private val compiledMessages: MutableList<CompiledMessageData> = arrayListOf()
165
166     override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation?) {
167         synchronized(compiledMessages) {
168             compiledMessages.add(CompiledMessageData(severity, message, location))
169         }
170     }
171
172     override fun hasErrors() =
173         synchronized(compiledMessages) {
174             compiledMessages.any { it.severity.isError }
175         }
176
177     override fun clear() {
178         synchronized(compiledMessages) {
179             compiledMessages.clear()
180         }
181     }
182
183     fun errors(): List<CompiledMessageData> = compiledMessages.filter { it.severity == CompilerMessageSeverity.ERROR }
184 }