/* * Copyright (C) 2019 The Android Open Source Project * * Licensed 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 com.android.protolog.tool import com.android.protolog.tool.CommandOptions.Companion.USAGE import com.github.javaparser.ParseProblemException import com.github.javaparser.ParserConfiguration import com.github.javaparser.StaticJavaParser import com.github.javaparser.ast.CompilationUnit import java.io.File import java.io.FileInputStream import java.io.FileOutputStream import java.io.OutputStream import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.jar.JarOutputStream import java.util.zip.ZipEntry import kotlin.system.exitProcess object ProtoLogTool { private fun showHelpAndExit() { println(USAGE) exitProcess(-1) } private fun containsProtoLogText(source: String, protoLogClassName: String): Boolean { val protoLogSimpleClassName = protoLogClassName.substringAfterLast('.') return source.contains(protoLogSimpleClassName) } private fun processClasses(command: CommandOptions) { val groups = injector.readLogGroups( command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg) val out = injector.fileOutputStream(command.outputSourceJarArg) val outJar = JarOutputStream(out) val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, command.protoLogGroupsClassNameArg, groups) val executor = newThreadPool() try { command.javaSourceArgs.map { path -> executor.submitCallable { val transformer = SourceTransformer(command.protoLogImplClassNameArg, command.protoLogCacheClassNameArg, processor) val file = File(path) val text = injector.readText(file) val outSrc = try { val code = tryParse(text, path) if (containsProtoLogText(text, command.protoLogClassNameArg)) { transformer.processClass(text, path, packagePath(file, code), code) } else { text } } catch (ex: ParsingException) { // If we cannot parse this file, skip it (and log why). Compilation will // fail in a subsequent build step. injector.reportParseError(ex) text } path to outSrc } }.map { future -> val (path, outSrc) = future.get() outJar.putNextEntry(ZipEntry(path)) outJar.write(outSrc.toByteArray()) outJar.closeEntry() } } finally { executor.shutdown() } val cacheSplit = command.protoLogCacheClassNameArg.split(".") val cacheName = cacheSplit.last() val cachePackage = cacheSplit.dropLast(1).joinToString(".") val cachePath = "gen/${cacheSplit.joinToString("/")}.java" outJar.putNextEntry(ZipEntry(cachePath)) outJar.write(generateLogGroupCache(cachePackage, cacheName, groups, command.protoLogImplClassNameArg, command.protoLogGroupsClassNameArg).toByteArray()) outJar.close() out.close() } fun generateLogGroupCache( cachePackage: String, cacheName: String, groups: Map, protoLogImplClassName: String, protoLogGroupsClassName: String ): String { val fields = groups.values.map { "public static boolean ${it.name}_enabled = false;" }.joinToString("\n") val updates = groups.values.map { "${it.name}_enabled = " + "$protoLogImplClassName.isEnabled($protoLogGroupsClassName.${it.name});" }.joinToString("\n") return """ package $cachePackage; public class $cacheName { ${fields.replaceIndent(" ")} static { $protoLogImplClassName.sCacheUpdater = $cacheName::update; update(); } static void update() { ${updates.replaceIndent(" ")} } } """.trimIndent() } private fun tryParse(code: String, fileName: String): CompilationUnit { try { return StaticJavaParser.parse(code) } catch (ex: ParseProblemException) { val problem = ex.problems.first() throw ParsingException("Java parsing erro" + "r: ${problem.verboseMessage}", ParsingContext(fileName, problem.location.orElse(null) ?.begin?.range?.orElse(null)?.begin?.line ?: 0)) } } private fun viewerConf(command: CommandOptions) { val groups = injector.readLogGroups( command.protoLogGroupsJarArg, command.protoLogGroupsClassNameArg) val processor = ProtoLogCallProcessor(command.protoLogClassNameArg, command.protoLogGroupsClassNameArg, groups) val builder = ViewerConfigBuilder(processor) val executor = newThreadPool() try { command.javaSourceArgs.map { path -> executor.submitCallable { val file = File(path) val text = injector.readText(file) if (containsProtoLogText(text, command.protoLogClassNameArg)) { try { val code = tryParse(text, path) builder.findLogCalls(code, path, packagePath(file, code)) } catch (ex: ParsingException) { // If we cannot parse this file, skip it (and log why). Compilation will // fail in a subsequent build step. injector.reportParseError(ex) null } } else { null } } }.forEach { future -> builder.addLogCalls(future.get() ?: return@forEach) } } finally { executor.shutdown() } val out = injector.fileOutputStream(command.viewerConfigJsonArg) out.write(builder.build().toByteArray()) out.close() } private fun packagePath(file: File, code: CompilationUnit): String { val pack = if (code.packageDeclaration.isPresent) code.packageDeclaration .get().nameAsString else "" val packagePath = pack.replace('.', '/') + '/' + file.name return packagePath } private fun read(command: CommandOptions) { LogParser(ViewerConfigParser()) .parse(FileInputStream(command.logProtofileArg), FileInputStream(command.viewerConfigJsonArg), System.out) } @JvmStatic fun main(args: Array) { try { val command = CommandOptions(args) invoke(command) } catch (ex: InvalidCommandException) { println("\n${ex.message}\n") showHelpAndExit() } catch (ex: CodeProcessingException) { println("\n${ex.message}\n") exitProcess(1) } } fun invoke(command: CommandOptions) { StaticJavaParser.setConfiguration(ParserConfiguration().apply { setLanguageLevel(ParserConfiguration.LanguageLevel.RAW) setAttributeComments(false) }) when (command.command) { CommandOptions.TRANSFORM_CALLS_CMD -> processClasses(command) CommandOptions.GENERATE_CONFIG_CMD -> viewerConf(command) CommandOptions.READ_LOG_CMD -> read(command) } } var injector = object : Injector { override fun fileOutputStream(file: String) = FileOutputStream(file) override fun readText(file: File) = file.readText() override fun readLogGroups(jarPath: String, className: String) = ProtoLogGroupReader().loadFromJar(jarPath, className) override fun reportParseError(ex: ParsingException) { println("\n${ex.message}\n") } } interface Injector { fun fileOutputStream(file: String): OutputStream fun readText(file: File): String fun readLogGroups(jarPath: String, className: String): Map fun reportParseError(ex: ParsingException) } } private fun ExecutorService.submitCallable(f: () -> T) = submit(f) private fun newThreadPool() = Executors.newFixedThreadPool( Runtime.getRuntime().availableProcessors())