1 /* 2 * 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 247 private fun <T> ExecutorService.submitCallable(f: () -> T) = submit(f) 248 249 private fun newThreadPool() = Executors.newFixedThreadPool( 250 Runtime.getRuntime().availableProcessors()) 251