package exh.log import com.elvishew.xlog.flattener.Flattener2 import com.elvishew.xlog.internal.DefaultsFactory import com.elvishew.xlog.printer.Printer import com.elvishew.xlog.printer.file.backup.BackupStrategy import com.elvishew.xlog.printer.file.clean.CleanStrategy import com.elvishew.xlog.printer.file.naming.FileNameGenerator import java.io.BufferedWriter import java.io.File import java.io.FileWriter import java.io.IOException import java.util.concurrent.BlockingQueue import java.util.concurrent.LinkedBlockingQueue /** * Log [Printer] using file system. When print a log, it will print it to the specified file. * * * Use the [Builder] to construct a [EnhancedFilePrinter] object. */ @Suppress("unused") class EnhancedFilePrinter internal constructor(builder: Builder) : Printer { /** * The folder path of log file. */ private val folderPath: String /** * The file name generator for log file. */ private val fileNameGenerator: FileNameGenerator /** * The backup strategy for log file. */ private val backupStrategy: BackupStrategy /** * The clean strategy for log file. */ private val cleanStrategy: CleanStrategy /** * The flattener when print a log. */ private val flattener: Flattener2 /** * Log writer. */ private val writer: Writer @Volatile private var worker: Worker? = null /** * Make sure the folder of log file exists. */ private fun checkLogFolder() { val folder = File(folderPath) if (!folder.exists()) { folder.mkdirs() } } override fun println(logLevel: Int, tag: String, msg: String) { val timeMillis = System.currentTimeMillis() if (USE_WORKER) { val worker = worker ?: return if (!worker.isStarted()) { worker.start() } worker.enqueue(LogItem(timeMillis, logLevel, tag, msg)) } else { doPrintln(timeMillis, logLevel, tag, msg) } } /** * Do the real job of writing log to file. */ private fun doPrintln(timeMillis: Long, logLevel: Int, tag: String, msg: String) { var lastFileName = writer.lastFileName if (lastFileName == null || fileNameGenerator.isFileNameChangeable) { val newFileName = fileNameGenerator.generateFileName(logLevel, System.currentTimeMillis()) require(!(newFileName == null || newFileName.trim { it <= ' ' }.isEmpty())) { "File name should not be empty." } if (newFileName != lastFileName) { if (writer.isOpened) { writer.close() } cleanLogFilesIfNecessary() if (writer.open(newFileName).not()) { return } lastFileName = newFileName } } val lastFile = writer.file ?: return if (backupStrategy.shouldBackup(lastFile)) { // Backup the log file, and create a new log file. writer.close() val backupFile = File(folderPath, "$lastFileName.bak") if (backupFile.exists()) { backupFile.delete() } lastFile.renameTo(backupFile) if (writer.open(lastFileName).not()) { return } } val flattenedLog = flattener.flatten(timeMillis, logLevel, tag, msg).toString() writer.appendLog(flattenedLog) } /** * Clean log files if should clean follow strategy */ private fun cleanLogFilesIfNecessary() { val logDir = File(folderPath) logDir.listFiles().orEmpty() .asSequence() .filter { cleanStrategy.shouldClean(it) } .forEach { it.delete() } } /** * Builder for [EnhancedFilePrinter]. */ class Builder /** * Construct a builder. * * @param folderPath the folder path of log file */( /** * The folder path of log file. */ val folderPath: String, ) { /** * The file name generator for log file. */ var fileNameGenerator: FileNameGenerator? = null /** * The backup strategy for log file. */ var backupStrategy: BackupStrategy? = null /** * The clean strategy for log file. */ var cleanStrategy: CleanStrategy? = null /** * The flattener when print a log. */ var flattener: Flattener2? = null /** * Set the file name generator for log file. * * @param fileNameGenerator the file name generator for log file * @return the builder */ fun fileNameGenerator(fileNameGenerator: FileNameGenerator): Builder { this.fileNameGenerator = fileNameGenerator return this } /** * Set the backup strategy for log file. * * @param backupStrategy the backup strategy for log file * @return the builder */ fun backupStrategy(backupStrategy: BackupStrategy): Builder { this.backupStrategy = backupStrategy return this } /** * Set the clean strategy for log file. * * @param cleanStrategy the clean strategy for log file * @return the builder */ fun cleanStrategy(cleanStrategy: CleanStrategy): Builder { this.cleanStrategy = cleanStrategy return this } /** * Set the flattener when print a log. * * @param flattener the flattener when print a log * @return the builder * @since 1.6.0 */ fun flattener(flattener: Flattener2): Builder { this.flattener = flattener return this } /** * Build configured [EnhancedFilePrinter] object. * * @return the built configured [EnhancedFilePrinter] object */ fun build(): EnhancedFilePrinter { fillEmptyFields() return EnhancedFilePrinter(this) } private fun fillEmptyFields() { if (fileNameGenerator == null) { fileNameGenerator = DefaultsFactory.createFileNameGenerator() } if (backupStrategy == null) { backupStrategy = DefaultsFactory.createBackupStrategy() } if (cleanStrategy == null) { cleanStrategy = DefaultsFactory.createCleanStrategy() } if (flattener == null) { flattener = DefaultsFactory.createFlattener2() } } } private class LogItem( var timeMillis: Long, var level: Int, var tag: String, var msg: String, ) /** * Work in background, we can enqueue the logs, and the worker will dispatch them. */ private inner class Worker : Runnable { private val logs: BlockingQueue = LinkedBlockingQueue() @Volatile private var started = false /** * Enqueue the log. * * @param log the log to be written to file */ fun enqueue(log: LogItem) { try { logs.put(log) } catch (e: InterruptedException) { e.printStackTrace() } } /** * Whether the worker is started. * * @return true if started, false otherwise */ fun isStarted(): Boolean { synchronized(this) { return started } } /** * Start the worker. */ fun start() { synchronized(this) { Thread(this).start() started = true } } override fun run() { var log: LogItem try { while (logs.take().also { log = it } != null) { doPrintln(log.timeMillis, log.level, log.tag, log.msg) } } catch (e: InterruptedException) { e.printStackTrace() synchronized(this) { started = false } } } } /** * Used to write the flattened logs to the log file. */ private inner class Writer { /** * Get the name of last used log file. * @return the name of last used log file, maybe null */ /** * The file name of last used log file. */ var lastFileName: String? = null private set /** * Get the current log file. * * @return the current log file, maybe null */ /** * The current log file. */ var file: File? = null private set private var bufferedWriter: BufferedWriter? = null /** * Whether the log file is opened. * * @return true if opened, false otherwise */ val isOpened: Boolean get() = bufferedWriter != null /** * Open the file of specific name to be written into. * * @param newFileName the specific file name * @return true if opened successfully, false otherwise */ fun open(newFileName: String): Boolean { return try { val file = File(folderPath, newFileName) if (file.exists().not()) { (file.parentFile ?: File(file.absolutePath.substringBeforeLast(File.separatorChar))).mkdirs() file.createNewFile() } bufferedWriter = FileWriter(file, true).buffered() lastFileName = newFileName this.file = file true } catch (e: Exception) { e.printStackTrace() false } } /** * Close the current log file if it is opened. * * @return true if closed successfully, false otherwise */ fun close(): Boolean { if (bufferedWriter != null) { try { bufferedWriter?.close() } catch (e: IOException) { e.printStackTrace() return false } finally { bufferedWriter = null lastFileName = null file = null } } return true } /** * Append the flattened log to the end of current opened log file. * * @param flattenedLog the flattened log */ fun appendLog(flattenedLog: String) { val bufferedWriter = bufferedWriter require(bufferedWriter != null) try { bufferedWriter.write(flattenedLog) bufferedWriter.newLine() bufferedWriter.flush() } catch (e: IOException) { } } } companion object { /** * Use worker, write logs asynchronously. */ private const val USE_WORKER = true } /*package*/ init { folderPath = builder.folderPath fileNameGenerator = builder.fileNameGenerator!! backupStrategy = builder.backupStrategy!! cleanStrategy = builder.cleanStrategy!! flattener = builder.flattener!! writer = Writer() if (USE_WORKER) { worker = Worker() } checkLogFolder() } }