1 /*
<lambda>null2 * Copyright (C) 2019 The Android Open Source Project
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 com.android.protolog.tool
18
19 import com.android.protolog.tool.CommandOptions.Companion.USAGE
20 import com.github.javaparser.ParseProblemException
21 import com.github.javaparser.ParserConfiguration
22 import com.github.javaparser.StaticJavaParser
23 import com.github.javaparser.ast.CompilationUnit
24 import java.io.File
25 import java.io.FileInputStream
26 import java.io.FileOutputStream
27 import java.io.OutputStream
28 import java.util.concurrent.ExecutorService
29 import java.util.concurrent.Executors
30 import java.util.jar.JarOutputStream
31 import java.util.zip.ZipEntry
32 import kotlin.system.exitProcess
33
34 object ProtoLogTool {
35 private fun showHelpAndExit() {
36 println(USAGE)
37 exitProcess(-1)
38 }
39
40 private fun containsProtoLogText(source: String, protoLogClassName: String): Boolean {
41 val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.')
42 return source.contains(protoLogSimpleClassName)
43 }
44
45 private fun processClasses(command: CommandOptions) {
46 val groups = injector.readLogGroups(
47 command.protoLogGroupsJarArg,
48 command.protoLogGroupsClassNameArg)
49 val out = injector.fileOutputStream(command.outputSourceJarArg)
50 val outJar = JarOutputStream(out)
51 val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
52 command.protoLogGroupsClassNameArg, groups)
53
54 val executor = newThreadPool()
55
56 try {
57 command.javaSourceArgs.map { path ->
58 executor.submitCallable {
59 val transformer = SourceTransformer(command.protoLogImplClassNameArg,
60 command.protoLogCacheClassNameArg, processor)
61 val file = File(path)
62 val text = injector.readText(file)
63 val outSrc = try {
64 val code = tryParse(text, path)
65 if (containsProtoLogText(text, command.protoLogClassNameArg)) {
66 transformer.processClass(text, path, packagePath(file, code), code)
67 } else {
68 text
69 }
70 } catch (ex: ParsingException) {
71 // If we cannot parse this file, skip it (and log why). Compilation will
72 // fail in a subsequent build step.
73 injector.reportParseError(ex)
74 text
75 }
76 path to outSrc
77 }
78 }.map { future ->
79 val (path, outSrc) = future.get()
80 outJar.putNextEntry(ZipEntry(path))
81 outJar.write(outSrc.toByteArray())
82 outJar.closeEntry()
83 }
84 } finally {
85 executor.shutdown()
86 }
87
88 val cacheSplit = command.protoLogCacheClassNameArg.split(".")
89 val cacheName = cacheSplit.last()
90 val cachePackage = cacheSplit.dropLast(1).joinToString(".")
91 val cachePath = "gen/${cacheSplit.joinToString("/")}.java"
92
93 outJar.putNextEntry(ZipEntry(cachePath))
94 outJar.write(generateLogGroupCache(cachePackage, cacheName, groups,
95 command.protoLogImplClassNameArg, command.protoLogGroupsClassNameArg).toByteArray())
96
97 outJar.close()
98 out.close()
99 }
100
101 fun generateLogGroupCache(
102 cachePackage: String,
103 cacheName: String,
104 groups: Map<String, LogGroup>,
105 protoLogImplClassName: String,
106 protoLogGroupsClassName: String
107 ): String {
108 val fields = groups.values.map {
109 "public static boolean ${it.name}_enabled = false;"
110 }.joinToString("\n")
111
112 val updates = groups.values.map {
113 "${it.name}_enabled = " +
114 "$protoLogImplClassName.isEnabled($protoLogGroupsClassName.${it.name});"
115 }.joinToString("\n")
116
117 return """
118 package $cachePackage;
119
120 public class $cacheName {
121 ${fields.replaceIndent(" ")}
122
123 static {
124 $protoLogImplClassName.sCacheUpdater = $cacheName::update;
125 update();
126 }
127
128 static void update() {
129 ${updates.replaceIndent(" ")}
130 }
131 }
132 """.trimIndent()
133 }
134
135 private fun tryParse(code: String, fileName: String): CompilationUnit {
136 try {
137 return StaticJavaParser.parse(code)
138 } catch (ex: ParseProblemException) {
139 val problem = ex.problems.first()
140 throw ParsingException("Java parsing erro" +
141 "r: ${problem.verboseMessage}",
142 ParsingContext(fileName, problem.location.orElse(null)
143 ?.begin?.range?.orElse(null)?.begin?.line
144 ?: 0))
145 }
146 }
147
148 private fun viewerConf(command: CommandOptions) {
149 val groups = injector.readLogGroups(
150 command.protoLogGroupsJarArg,
151 command.protoLogGroupsClassNameArg)
152 val processor = ProtoLogCallProcessor(command.protoLogClassNameArg,
153 command.protoLogGroupsClassNameArg, groups)
154 val builder = ViewerConfigBuilder(processor)
155
156 val executor = newThreadPool()
157
158 try {
159 command.javaSourceArgs.map { path ->
160 executor.submitCallable {
161 val file = File(path)
162 val text = injector.readText(file)
163 if (containsProtoLogText(text, command.protoLogClassNameArg)) {
164 try {
165 val code = tryParse(text, path)
166 builder.findLogCalls(code, path, packagePath(file, code))
167 } catch (ex: ParsingException) {
168 // If we cannot parse this file, skip it (and log why). Compilation will
169 // fail in a subsequent build step.
170 injector.reportParseError(ex)
171 null
172 }
173 } else {
174 null
175 }
176 }
177 }.forEach { future ->
178 builder.addLogCalls(future.get() ?: return@forEach)
179 }
180 } finally {
181 executor.shutdown()
182 }
183
184 val out = injector.fileOutputStream(command.viewerConfigJsonArg)
185 out.write(builder.build().toByteArray())
186 out.close()
187 }
188
189 private fun packagePath(file: File, code: CompilationUnit): String {
190 val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration
191 .get().nameAsString else ""
192 val packagePath = pack.replace('.', '/') + '/' + file.name
193 return packagePath
194 }
195
196 private fun read(command: CommandOptions) {
197 LogParser(ViewerConfigParser())
198 .parse(FileInputStream(command.logProtofileArg),
199 FileInputStream(command.viewerConfigJsonArg), System.out)
200 }
201
202 @JvmStatic
203 fun main(args: Array<String>) {
204 try {
205 val command = CommandOptions(args)
206 invoke(command)
207 } catch (ex: InvalidCommandException) {
208 println("\n${ex.message}\n")
209 showHelpAndExit()
210 } catch (ex: CodeProcessingException) {
211 println("\n${ex.message}\n")
212 exitProcess(1)
213 }
214 }
215
216 fun invoke(command: CommandOptions) {
217 StaticJavaParser.setConfiguration(ParserConfiguration().apply {
218 setLanguageLevel(ParserConfiguration.LanguageLevel.RAW)
219 setAttributeComments(false)
220 })
221
222 when (command.command) {
223 CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command)
224 CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command)
225 CommandOptions.READ_LOG_CMD -> read(command)
226 }
227 }
228
229 var injector = object : Injector {
230 override fun fileOutputStream(file: String) = FileOutputStream(file)
231 override fun readText(file: File) = file.readText()
232 override fun readLogGroups(jarPath: String, className: String) =
233 ProtoLogGroupReader().loadFromJar(jarPath, className)
234 override fun reportParseError(ex: ParsingException) {
235 println("\n${ex.message}\n")
236 }
237 }
238
239 interface Injector {
240 fun fileOutputStream(file: String): OutputStream
241 fun readText(file: File): String
242 fun readLogGroups(jarPath: String, className: String): Map<String, LogGroup>
243 fun reportParseError(ex: ParsingException)
244 }
245 }
246
submitCallablenull247 private fun <T> ExecutorService.submitCallable(f: () -> T) = submit(f)
248
249 private fun newThreadPool() = Executors.newFixedThreadPool(
250 Runtime.getRuntime().availableProcessors())
251