/*
* Copyright 2015-2017 Austin Keener & Michael Ritter
*
* 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 net.dv8tion.jda.core.utils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import javax.swing.JOptionPane;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.text.SimpleDateFormat;
import java.util.*;
public class SimpleLog
{
/**
* The global LOG-level that is used as standard if not overwritten
*/
public static Level LEVEL = Level.INFO;
/**
* If this boolean is set to true, if there is no console present, messages are shown as message-dialogs
*/
public static boolean ENABLE_GUI = false;
private static final String FORMAT = "[%time%] [%level%] [%name%]: %text%";
private static final String MSGFORMAT = "%text%";
private static final SimpleDateFormat DFORMAT = new SimpleDateFormat("HH:mm:ss");
private static final Map<String, SimpleLog> LOGS = new HashMap<>();
private static final Set<LogListener> listeners = new HashSet<>();
/**
* Will get the LOG with the given LOG-name or create one if it didn't exist
*
* @param name the name of the LOG
* @return SimpleLog with given LOG-name
*/
public static SimpleLog getLog(String name) {
synchronized (LOGS) {
if(!LOGS.containsKey(name.toLowerCase())) {
LOGS.put(name.toLowerCase(), new SimpleLog(name));
}
}
return LOGS.get(name.toLowerCase());
}
private static final Map<Level, Set<File>> fileLogs = new HashMap<>();
private static PrintStream origStd = null;
private static PrintStream origErr = null;
private static FileOutputStream stdOut = null;
private static FileOutputStream errOut = null;
/**
* Will duplicate the output-streams to the specified Files.
* This will catch everything that is sent to sout and serr (even stuff not logged via SimpleLog).
*
* @param std
* The file to use for System.out logging, or null to not LOG System.out to a file
* @param err
* The file to use for System.err logging, or null to not LOG System.err to a file
* @throws java.io.IOException
* If an IO error is encountered while dealing with the file. Most likely
* to be caused by a lack of permissions when creating the log folders or files.
*/
@SuppressWarnings("ResultOfMethodCallIgnored")
public static void addFileLogs(File std, File err) throws IOException {
if(std != null) {
if (origStd == null)
origStd = System.out;
if(!std.getAbsoluteFile().getParentFile().exists()) {
std.getAbsoluteFile().getParentFile().mkdirs();
}
if(!std.exists()) {
std.createNewFile();
}
FileOutputStream fOut = new FileOutputStream(std, true);
System.setOut(new PrintStream(new OutputStream() {
@Override
public void write(int b) throws IOException {
origStd.write(b);
fOut.write(b);
}
}));
if (stdOut != null)
stdOut.close();
stdOut = fOut;
}
else if (origStd != null)
{
System.setOut(origStd);
stdOut.close();
origStd = null;
}
if(err != null) {
if (origErr == null)
origErr = System.err;
if(!err.getAbsoluteFile().getParentFile().exists()) {
err.getAbsoluteFile().getParentFile().mkdirs();
}
if(!err.exists()) {
err.createNewFile();
}
FileOutputStream fOut = new FileOutputStream(err, true);
System.setErr(new PrintStream(new OutputStream() {
@Override
public void write(int b) throws IOException {
origErr.write(b);
fOut.write(b);
}
}));
if (errOut != null)
errOut.close();
errOut = fOut;
}
else if (origErr != null)
{
System.setErr(origErr);
errOut.close();
origErr = null;
}
}
/**
* Sets up a File to log all messages that are not visible via sout and serr and meet a given log-level criteria.
* All logs that would not be printed and are above given logLevel are printed to that File.
* If you want to log Logs printed to sout and serr to file(s), use {@link #addFileLogs(java.io.File, java.io.File)} instead.
*
* @param logLevel
* The log-level criteria. Only logs equal or above this level are printed to the File
* @param file
* The File where the logs should be printed
* @throws java.io.IOException
* If the File can't be canonically resolved (access denied)
*/
public static void addFileLog(Level logLevel, File file) throws IOException
{
File canonicalFile = file.getCanonicalFile();
if (!fileLogs.containsKey(logLevel))
{
fileLogs.put(logLevel, new HashSet<>());
}
fileLogs.get(logLevel).add(canonicalFile);
}
/**
* Removes all File-logs created via {@link #addFileLog(net.dv8tion.jda.core.utils.SimpleLog.Level, java.io.File)} with given Level.
* To remove the sout and serr logs, call {@link #addFileLogs(java.io.File, java.io.File)} with null args.
*
* @param logLevel
* The level for which all fileLogs should be removed
*/
public static void removeFileLog(Level logLevel)
{
fileLogs.remove(logLevel);
}
/**
* Removes all File-logs created via {@link #addFileLog(net.dv8tion.jda.core.utils.SimpleLog.Level, java.io.File)} with given File.
* To remove the sout and serr logs, call {@link #addFileLogs(java.io.File, java.io.File)} with null args.
*
* @param file
* The file to remove from all FileLogs (except sout and serr logs)
* @throws java.io.IOException
* If the File can't be canonically resolved (access denied)
*/
public static void removeFileLog(File file) throws IOException
{
File canonicalFile = file.getCanonicalFile();
Iterator<Map.Entry<Level, Set<File>>> setIter = fileLogs.entrySet().iterator();
while (setIter.hasNext())
{
Map.Entry<Level, Set<File>> set = setIter.next();
Iterator<File> fileIter = set.getValue().iterator();
while (fileIter.hasNext())
{
File logFile = fileIter.next();
if(logFile.equals(canonicalFile))
{
fileIter.remove();
break;
}
}
if (set.getValue().isEmpty())
{
setIter.remove();
}
}
}
private static Set<File> collectFiles(Level level)
{
Set<File> out = new HashSet<>();
for (Map.Entry<Level, Set<File>> mapEntry : fileLogs.entrySet())
{
if(mapEntry.getKey().getPriority() <= level.getPriority())
out.addAll(mapEntry.getValue());
}
return out;
}
private static void logToFiles(String msg, Level level)
{
Set<File> files = collectFiles(level);
for (File file : files)
{
try
{
Files.write(file.toPath(), (msg + '\n').getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
catch (IOException e)
{
e.printStackTrace();
// JDAImpl.LOG.fatal("Could not write log to logFile...");
// JDAImpl.LOG.log(e);
}
}
}
/**
* Adds a custom Listener that receives all logs
* @param listener the listener to add
*/
public static void addListener(LogListener listener)
{
synchronized (listeners)
{
listeners.add(listener);
}
}
/**
* Removes a custom Listener
* @param listener the listener to remove
*/
public static void removeListener(LogListener listener)
{
synchronized (listeners)
{
listeners.remove(listener);
}
}
public final String name;
private Level level = null;
private SimpleLog(String name) {
this.name = name;
}
/**
* Set the LOG-level
* All messages with lower LOG-level will not be printed
* If this level is set to null, the global Log-level ({@link net.dv8tion.jda.core.utils.SimpleLog#LEVEL}) will be used
*
* @param lev the new LOG-level
*/
public void setLevel(Level lev) {
this.level = lev;
}
/**
* Gets the current logging-level of this Logger.
* This might return null, if the global logging-level is used.
*
* @return the logging-level of this Logger or null
*/
public Level getLevel()
{
return level;
}
/**
* Gets the effective logging-level of this Logger.
* This considers the global logging-level.
*
* @return the effective logging-level of this Logger
*/
public Level getEffectiveLevel()
{
return level == null ? SimpleLog.LEVEL : level;
}
/**
* Will LOG a message with given LOG-level
*
* @param level The level of the Log
* @param msg The message to LOG
*/
public void log(Level level, Object msg) {
synchronized (listeners)
{
for (LogListener listener : listeners)
{
listener.onLog(this, level, msg);
}
}
String format = (ENABLE_GUI && !isConsolePresent()) ? MSGFORMAT : FORMAT;
format = format.replace("%time%", DFORMAT.format(new Date())).replace("%level%", level.getTag()).replace("%name%", name).replace("%text%", String.valueOf(msg));
if(level == Level.OFF || level.getPriority() < ((this.level == null) ? SimpleLog.LEVEL.getPriority() : this.level.getPriority())) {
logToFiles(format, level);
}
else
{
print(format, level);
}
}
public void log(Throwable ex)
{
synchronized (listeners)
{
for (LogListener listener : listeners)
{
listener.onError(this, ex);
}
}
log(Level.FATAL, "Encountered an exception:");
log(Level.FATAL, ExceptionUtils.getStackTrace(ex));
}
/**
* Will LOG a message with trace level.
*
* @param msg the object, which should be logged
*/
public void trace(Object msg) {
log(Level.TRACE, msg);
}
/**
* Will LOG a message with debug level
*
* @param msg the object, which should be logged
*/
public void debug(Object msg) {
log(Level.DEBUG, msg);
}
/**
* Will LOG a message with info level
*
* @param msg the object, which should be logged
*/
public void info(Object msg) {
log(Level.INFO, msg);
}
/**
* Will LOG a message with warning level
*
* @param msg the object, which should be logged
*/
public void warn(Object msg) {
log(Level.WARNING, msg);
}
/**
* Will LOG a message with fatal level
*
* @param msg the object, which should be logged
*/
public void fatal(Object msg) {
log(Level.FATAL, msg);
}
/**
* prints a message to the console or as message-box.
*
* @param msg the message, that should be displayed
* @param level the LOG level of the message
*/
private void print(String msg, Level level) {
if(ENABLE_GUI && !isConsolePresent()) {
if(level.isError()) {
JOptionPane.showMessageDialog(null, msg, "An Error occurred!", JOptionPane.ERROR_MESSAGE);
} else {
JOptionPane.showMessageDialog(null, msg, level.getTag(), JOptionPane.INFORMATION_MESSAGE);
}
} else {
if(level.isError()) {
System.err.println(msg);
} else {
System.out.println(msg);
}
}
}
/**
* Will return whether the program has a console present, or was launched without
*
* @return boolean true, if console is present
*/
public static boolean isConsolePresent() {
return System.console() != null;
}
/**
* This interface has to be able to register (via {@link net.dv8tion.jda.core.utils.SimpleLog#addListener(net.dv8tion.jda.core.utils.SimpleLog.LogListener)}) and listen to log-messages.
*/
public interface LogListener
{
/**
* Called on any incoming log-messages (including stacktraces).
* This is also called on log-messages that would normally not print to console due to log-level.
* @param log
* the log this message was sent to
* @param logLevel
* the level of the message sent
* @param message
* the message as object
*/
void onLog(SimpleLog log, Level logLevel, Object message);
/**
* Called on any incoming error-message (Throwable).
* Note: Throwables are also logged with FATAL level.
* This is just a convenience Method to do special Throwable handling.
* @param log
* the log this error was sent to
* @param err
* the error as Throwable
*/
void onError(SimpleLog log, Throwable err);
}
/**
* Enum containing all the LOG-levels
*/
public enum Level {
ALL("Finest", 0, false),
TRACE("Trace", 1, false),
DEBUG("Debug", 2, false),
INFO("Info", 3, false),
WARNING("Warning", 4, true),
FATAL("Fatal", 5, true),
OFF("NO-LOGGING", 6, true);
private final String msg;
private final int pri;
private final boolean isError;
Level(String message, int priority, boolean isError) {
this.msg = message;
this.pri = priority;
this.isError = isError;
}
/**
* Returns the Log-Tag (e.g. Fatal)
*
* @return the logTag
*/
public String getTag() {
return msg;
}
/**
* Returns the numeric priority of this loglevel, with 0 being the lowest
*
* @return the level-priority
*/
public int getPriority() {
return pri;
}
/**
* Returns whether this LOG-level should be treated like an error or not
*
* @return boolean true, if this LOG-level is an error-level
*/
public boolean isError() {
return isError;
}
}
}
Comments