package com.mattc.autotyper.util; import com.google.common.base.Strings; import com.mattc.autotyper.Ref; import com.mattc.autotyper.meta.IORuntimeException; import org.apache.log4j.Appender; import org.apache.log4j.ConsoleAppender; import org.apache.log4j.EnhancedPatternLayout; import org.apache.log4j.Layout; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.RollingFileAppender; import org.joda.time.DateTime; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.regex.Pattern; /** * A Centralized Logging utility utilizing Log4j. <br /> * <br /> * By default this application attempts to maintain a Log in the Console and <br /> * a File Log on every run. It will, by default, maintain a maximum of 6 Log Files. <br /> * <br /> * Additional Logs can be created using the {@link #addLogTarget(Appender)} * method. * Alternatively <br /> * a PrintStream connected to this log can be obtained using * {@link #getLogPrintStream(boolean)}. * * @author Matthew */ public final class Console { private static final Logger logger; private static final Path logFile; private static PrintStream logStream; private static PrintStream combinedStream; public static final Path LOG_DIR = Paths.get(".", "logs"); /** * Max Number of .log Files allowed to exist in logs directory */ public static final int MAX_LOG_COUNT = 6; /** * The Pattern Passed to EnhancedPatternLayout for Log Formatting */ public static final String LOG_PATTERN = "%d{HH:mm:ss} - [%-5t][%-5p]: %m%n"; static { final DateTime cur = new DateTime(); final String logName = String.format("%s-%s-%s-%s-%s#%s#%s.log", Ref.APP_NAME, cur.getMonthOfYear(), cur.getDayOfMonth(), cur.getYearOfCentury(), cur.getHourOfDay(), cur.getMinuteOfHour(), cur.getSecondOfMinute()); final Layout layout = new EnhancedPatternLayout(LOG_PATTERN); logFile = LOG_DIR.resolve(logName); Thread.currentThread().setName(Ref.APP_NAME); logger = Logger.getLogger(Ref.APP_NAME); logger.setLevel(Level.ALL); try { final Appender fileApp = new RollingFileAppender(layout, logFile.toString()); final Appender consApp = new ConsoleAppender(layout); fileApp.setName(Ref.APP_NAME + "-LogFileAppender"); logger.addAppender(fileApp); logger.addAppender(consApp); initialize(); } catch (final IOException e) { e.printStackTrace(); } } public static void logToFile(Object msg) { synchronized (logger) { getLogPrintStream(false).print(String.valueOf(msg)); } } /** * Does the same as {@link #log(Object, Level)} but goes further by taking a * Throwable and <br /> * passing it to {@link Logger#log(org.apache.log4j.Priority, Object, * Throwable)} * to be parsed. <br /> * <br /> * The Throwable is parsed independently by each Appender. <br /> * In a normal use case this is ConsoleAppender and RollingFileAppender. <br /> */ public static void log(Object msg, Throwable t, Level level) { synchronized (logger) { logger.log(level, String.valueOf(msg), t); } } /** * Takes an Object as a message and obtains its String Value <br /> * via {@link String#valueOf(Object)} which is null-safe. <br /> * <br /> * The String Message and Level are passed to * {@link Logger#log(org.apache.log4j.Priority, Object)} <br /> * to be printed to the Console and Log File (If created) <br /> */ public static void log(Object msg, Level level) { synchronized (logger) { logger.log(level, String.valueOf(msg)); } } public static void empty() { info(""); } /** * Indicates a certain point has been reached successfully and may <br /> * print state information. <br /> * <br /> * This is the Level that should be generally used in normal cases. <br /> */ public static void info(Object msg) { log(msg, Level.INFO); } /** * Indicates a Debug Message meant for the Programmer alone. */ public static void debug(Object msg) { log(msg, Level.DEBUG); } /** * Write a trace to the log with no extra information. */ public static void trace() { trace(""); } /** * Write a Stack Trace to the log with the given additional message. Writes 16 * Stack Trace Elements at most. */ public static void trace(Object msg) { int lines = 0; final Thread current = Thread.currentThread(); final StackTraceElement[] trace = current.getStackTrace(); final String border = "========================================"; debug(""); debug(border); debug(String.format("|| Detailed Stack Trace of %s[%s] Thread", current.getName(), current.getId())); debug("|| Trace Message: " + String.valueOf(msg)); debug(border); for (int i = 2; (i < 18) && (i < trace.length); i++) { debug(String.format("|| at %s%s", trace[i].toString(), (i < 15) && (i < (trace.length - 1)) ? "..." : "")); lines++; } debug(border); debug("|| Resolved " + lines + " elements..."); debug(border); debug(""); } /** * Does the same as {@link #bigWarning(Object)} but prints "null" as the message. */ public static void bigWarning() { bigWarning(""); } /** * Prints a very noticeable warning that is bordered. <br /> * <br /> * Indicates the same thing as {@link #warn(Object)} but also prints<br /> * a 6 line Stack Trace (will not include the call to this method). <br /> * <br /> * <code> * **************************************** <br/> * * Message Here<br/> * * at trace(class:line)<br/> * * at trace(class:line)<br/> * * at trace(class:line)<br/> * * at trace(class:line)<br/> * * at trace(class:line)<br/> * * at trace(class:Line)<br/> * **************************************** <br/> * </code>Much Thanks to the Minecraft Forge Team for the idea! * * @param msg */ public static void bigWarning(Object msg) { final StackTraceElement[] trace = Thread.currentThread().getStackTrace(); final String border = "****************************************"; warn(""); warn(border); warn("* Warning! - " + String.valueOf(msg)); warn(border); for (int i = 2; (i < 8) && (i < trace.length); i++) { warn(String.format("* at %s%s", trace[i].toString(), (i < 7) && (i < (trace.length - 1)) ? "..." : "")); } warn(border); warn(""); } /** * Indicates that although the program can continue as expected, <br /> * the program may act unexpectedly due to receiving a valid, but <br /> * unexpected result or value. */ public static void warn(Object msg) { log(msg, Level.WARN); } /** * Indicates an Error that is recoverable but should be noted to the <br /> * user or programmer since this is likely a programmer error. <br /> */ public static void error(Object msg) { log(msg, Level.ERROR); } /** * Indicates a Fatal Error that has caused the program to terminate <br /> * since the error is unrecoverable. */ public static void fatal(Object msg) { log(msg, Level.FATAL); } /** * Write an Exception to the Log with the full Stack Trace. */ public static void exception(Throwable e) { exception(e, null); } /** * Write an Exception to the Log with the full Stack Trace and the given details. */ public static void exception(Throwable e, Object details) { final StackTraceElement[] elements = e.getStackTrace(); final String header = "===============EXCEPTION==============="; final String separator = "======================================="; final Throwable t = e.getCause(); error(header); error("Exception of type " + e.getClass().getName() + " caught!"); error("Error Message: " + e.getLocalizedMessage()); if (details != null) { error(String.valueOf(details)); } error(separator); error("Stack Trace: "); error(" " + elements[0]); for (int i = 1; i < elements.length; i++) { error(" \t" + elements[i]); } if (t != null) { cause(t, t.getLocalizedMessage() + " causing a " + e.getClass().getName(), 1); } error(header); } private static void cause(Throwable t, String details, int depth) { final StackTraceElement[] elements = t.getStackTrace(); final String tabs = Strings.repeat("\t", depth); final String causer = tabs + "===============CAUSED BY==============="; final String separator = tabs + "======================================="; final Throwable cause = t.getCause(); error(causer); error(tabs + "Exception of type " + t.getClass().getName() + "!"); error(tabs + "Error Message: " + t.getLocalizedMessage()); if (details != null) { error(tabs + String.valueOf(details)); } error(separator); error(tabs + "Stack Trace: "); error(tabs + " " + elements[0]); for (int i = 1; i < elements.length; i++) { error(" " + tabs + "\t" + elements[i]); } if (cause != null) { cause(cause, cause.getLocalizedMessage() + " causing a " + t.getClass().getName(), ++depth); } error(causer); } /** * Add an Appender as a another Logging Target in addition to <br /> * the Console and Log File. */ public static synchronized void addLogTarget(Appender appender) { logger.addAppender(appender); } /** * Ensures there are no more than MAX_LOG_COUNT files in the Logs directory. <br /> * <br /> * This deletes by age and attempts to delete the file immediately. If that <br /> * fails, then File.deleteOnExit() is called and the program resumes. This <br /> * is likely a temporary implementation since, at the moment, a user editing <br /> * or opening a log file will likely cause the file.lastModified() value to <br /> * change. This may or may not change in the future to account for said problem. */ private static void initialize() throws IOException { Path[] paths = Files.list(LOG_DIR) .filter((path) -> path.getFileName().toString().endsWith(".log")) .sorted((p1, p2) -> { try { return Files.getLastModifiedTime(p1).compareTo(Files.getLastModifiedTime(p2)); } catch (IOException e) { throw new IORuntimeException(e.getMessage(), e); } }).toArray(Path[]::new); // Delete Log Files until we have 10 or Less int size = paths.length; for (int i = 0; (size > MAX_LOG_COUNT) && (i < paths.length); i++) { Files.delete(paths[i]); size--; } } /** * Get a PrintStream connected to this Logging Utility. <br /> * <br /> * If combined == true then a PrintStream will be returned that will print to <br /> * all Appenders which would be the Console, Log File and any Appenders added * using {@link #addLogTarget(Appender)}. <br /> * <br /> * If combined == false then a PrintStream will be returned connected ONLY to the * Log File. * * @param combined - Whether or not to return a "combined" PrintStream */ public static synchronized PrintStream getLogPrintStream(boolean combined) { // This is really simple. // We only want ONE instance of these streams to EVER exist. if (combined) { if (combinedStream == null) { // When Printing simply pass the text to the Logger // let it handle formatting. combinedStream = new PrintStream(System.out) { // Pre-compile End Line Pattern Pattern pattern = Pattern.compile("(\r?\n)+"); @Override public void print(String s) { Console.info(s); } @Override public void print(Object obj) { Console.info(obj); } @Override public void println(String s) { // Since having Line Endings would destroy the look of the // Log // replace all of the line endings with " " Console.info(this.pattern.matcher(s).replaceAll(" ")); } @Override public void println(Object obj) { println(String.valueOf(obj)); } }; } return combinedStream; } else { if (logStream == null) { try { // Even Easier, Set up a PrintStream for the LogFile only. // Simply alter it to use a separate logger. logStream = new PrintStream(logFile.toFile()) { Logger logger = Logger.getLogger(Ref.APP_NAME + "-Stream"); // Stream // Logger Pattern pattern = Pattern.compile("(\r?\n)+"); // Pre-Compiled // Pattern // On Initialization, let the new Logger use the old Loggers // RollingFileAppender { logger.addAppender(Console.logger.getAppender(Ref.APP_NAME + "-LogFileAppender")); } @Override public void print(String s) { this.logger.info(s); } @Override public void print(Object obj) { print(String.valueOf(obj)); } @Override public void println(String s) { // Replace All \r or \n or \r\n with // " " to prevent it of destroying log readability s = this.pattern.matcher(s).replaceAll(" "); this.logger.info(s); } @Override public void println(Object obj) { println(String.valueOf(obj)); } }; } catch (final FileNotFoundException e) { Console.exception(e); } } return logStream; } } }