code: package zemberek.core.logging;
  
  import com.google.common.base.Joiner;
  import com.google.common.base.Splitter;
  import java.io.File;
  import java.io.IOException;
  import java.io.OutputStream;
  import java.io.PrintWriter;
  import java.io.StringWriter;
  import java.nio.file.Path;
  import java.text.SimpleDateFormat;
  import java.util.Date;
  import java.util.HashMap;
  import java.util.IllegalFormatException;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  import java.util.concurrent.ConcurrentHashMap;
  import java.util.logging.ConsoleHandler;
  import java.util.logging.FileHandler;
  import java.util.logging.Formatter;
  import java.util.logging.Handler;
  import java.util.logging.Level;
  import java.util.logging.LogRecord;
  import java.util.logging.Logger;
  import java.util.stream.Collectors;
  import zemberek.core.io.Strings;
  
  /**
   * A convenient Log class.
   */
  public final class Log {
  
  
    static final CUSTOM_FORMAT formatter = new CUSTOM_FORMAT();
    private static final Logger logger = Logger.getLogger("zemberek-logger");
    private static final Map<File, FileHandler> fileHandlers = new ConcurrentHashMap<>();
    private static final Map<String, LogLevel> classLevelMap = new ConcurrentHashMap<>();
    public static OutputStream fileStream;
    static Level currentLevel = Level.INFO;
    static SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss.SSS", Locale.ENGLISH);
    static Map<Level, String> levelShortStringMap = new HashMap<>();
  
    static {
      reset();
    }
  
    public static void reset() {
      logger.setUseParentHandlers(false);
      ConsoleHandler ch = new ConsoleHandler();
      ch.setFormatter(new CUSTOM_FORMAT());
      ch.setLevel(currentLevel);
      logger.addHandler(ch);
      Runtime.getRuntime()
          .addShutdownHook(new Thread(() -> fileHandlers.values().forEach(FileHandler::close)));
    }
  
    static {
      levelShortStringMap.put(Level.FINEST, "-");
      levelShortStringMap.put(Level.FINER, "T");
      levelShortStringMap.put(Level.FINE, "D");
      levelShortStringMap.put(Level.CONFIG, "-");
      levelShortStringMap.put(Level.OFF, "-");
      levelShortStringMap.put(Level.ALL, "-");
      levelShortStringMap.put(Level.WARNING, "W");
      levelShortStringMap.put(Level.INFO, "I");
      levelShortStringMap.put(Level.SEVERE, "E");
    }
  
    private Log() {
    }
  
    public static void addClassLevel(Class clazz, LogLevel level) {
      classLevelMap.put(clazz.getName(), level);
    }
  
    public static void removeClassLevel(Class clazz, LogLevel level) {
      classLevelMap.remove(clazz.getName(), level);
    }
  
    private static void setLevel(Level level) {
      logger.setLevel(level);
      for (Handler handler : logger.getHandlers()) {
        handler.setLevel(level);
      }
      currentLevel = level;
    }
  
    /**
     * @return true if Log is in debug level.
     */
    public static boolean isDebug() {
      return checkLevel(Level.FINE);
    }
  
    /**
     * @return true if Log is in trace level.
     */
    public static boolean isTrace() {
      return checkLevel(Level.FINER);
    }
  
    /**
     * @return true if Log is in info level.
     */
    public static boolean isInfo() {
      return checkLevel(Level.INFO);
    }
  
    /**
     * @return true if Log is in warn level.
     */
    public static boolean isWarn() {
      return checkLevel(Level.WARNING);
    }
  
    /**
     * @return true if Log is in trace level.
     */
    public static boolean isError() {
      return checkLevel(Level.SEVERE);
    }
  
    private static boolean checkLevel(Level level) {
      return currentLevel.intValue() <= level.intValue();
    }
  
    /**
     * Trace level logging. This can be used for cases finer grained than debug level.
     *
     * @param message message to log. it can be in String.format(message) format.
     * @param params if message is formatted according to String.format() params should contain
     * necessary arguments. If last parameter is a throwable, it is handled specially.
     */
    public static void trace(String message, Object... params) {
      log(Level.FINER, message, params);
    }
  
    public static void trace(boolean condition, String message, Object... params) {
      if (condition) {
        log(Level.FINER, message, params);
      }
    }
  
    public static void trace(Object object) {
      log(Level.FINER, object.toString());
    }
  
    public static void debug(String message, Object... params) {
      log(Level.FINE, message, params);
    }
  
    public static void debug(boolean condition, String message, Object... params) {
      if (condition) {
        log(Level.FINE, message, params);
      }
    }
  
    public static void debug(Object object) {
      log(Level.FINE, object.toString());
    }
  
    public static void info() {
      log(Level.INFO, "");
    }
  
    public static void info(String message, Object... params) {
      log(Level.INFO, message, params);
    }
  
    public static void info(boolean condition, String message, Object... params) {
      if (condition) {
        log(Level.INFO, message, params);
      }
    }
  
    public static void info(Object object) {
      log(Level.INFO, object.toString());
    }
  
    public static void warn(String message, Object... params) {
      log(Level.WARNING, message, params);
    }
  
    public static void exception(Throwable t) {
      logger.log(Level.SEVERE, "Exception occurred.", t);
    }
  
    public static void warn(boolean condition, String message, Object... params) {
      if (condition) {
        log(Level.WARNING, message, params);
      }
    }
  
    public static void warn(Object object) {
      log(Level.WARNING, object.toString());
    }
  
    public static void error(String message, Object... params) {
      log(Level.SEVERE, message, params);
    }
  
    public static void error(boolean condition, String message, Object... params) {
      if (condition) {
        log(Level.SEVERE, message, params);
      }
    }
  
    public static void error(Object object) {
      log(Level.SEVERE, object.toString());
    }
  
    public static void setError() {
      setLevel(Level.SEVERE);
    }
  
    public static void setWarn() {
      setLevel(Level.WARNING);
    }
  
    public static void setInfo() {
      setLevel(Level.INFO);
    }
  
    public static void setDebug() {
      setLevel(Level.FINE);
    }
  
    public static void setTrace() {
      setLevel(Level.FINER);
    }
  
    public static void addFileHandler(Path path) throws IOException {
  
      if (fileHandlers.containsKey(path.toFile())) {
        Log.info("Log File %s already exist. Appending.", path.toFile());
        return;
      }
  
      final FileHandler handler = new FileHandler(path.toFile().getAbsolutePath(), true);
      handler.setFormatter(formatter);
      handler.setLevel(currentLevel);
      logger.addHandler(handler);
      fileHandlers.put(path.toFile(), handler);
    }
  
    public static void flushFileHandlers() {
      fileHandlers.values().stream().filter(Objects::nonNull).forEach(Handler::flush);
    }
  
    public static void removeFileHandler(File file) {
      if (fileHandlers.containsKey(file)) {
        FileHandler handler = fileHandlers.remove(file);
        logger.removeHandler(handler);
      }
    }
  
    public static void removeFileHandler(Path path) {
      removeFileHandler(path.toFile());
    }
  
    public static List<Path> getCurrentLogFiles() {
      return fileHandlers.keySet().stream().map(File::toPath).collect(Collectors.toList());
    }
  
    public static synchronized void log(Level level, String message, Object... params) {
      final int stackPositionOfCaller = 2;
      StackTraceElement caller = new Throwable().getStackTrace()[stackPositionOfCaller];
      String className = caller.getClassName();
  
      boolean logIt = false;
  
      Level temp = currentLevel;
      if (classLevelMap.containsKey(className)) {
        Log.setLevel(classLevelMap.get(className).level);
      }
  
      if (logger.isLoggable(level)) {
        logIt = true;
      }
  
      if (!logIt) {
        return;
      }
  
      Throwable thrown = null;
      if (params.length > 0) {
        Object last = params[params.length - 1];
        if (last instanceof Throwable) {
          if (params.length > 1) {
            Object[] subParams = new Object[params.length - 1];
            System.arraycopy(params, 0, subParams, 0, subParams.length);
            params = subParams;
          }
          thrown = (Throwable) last;
        }
      }
      LogRecord record = new LogRecord(level, message);
      record.setLoggerName(logger.getName());
      record.setSourceClassName(className);
      record.setSourceMethodName(caller.getMethodName());
      record.setThrown(thrown);
      record.setParameters(params);
      logger.log(record);
      Log.setLevel(temp);
  
    }
  
    private static String shortenName(String name, int length) {
      if (name.length() < length) {
        return Strings.rightPad(name, length);
      } else {
        return name.substring(0, length - 1) + "~";
      }
    }
  
    private static String padIfNecessary(String name, int length) {
      if (name.length() < length) {
        return Strings.rightPad(name, length);
      }
      return name;
    }
  
    public enum LogLevel {
      TRACE(Level.FINER),
      DEBUG(Level.FINE),
      INFO(Level.INFO),
      WARNING(Level.WARNING),
      ERROR(Level.SEVERE);
  
      Level level;
  
      LogLevel(Level level) {
        this.level = level;
      }
    }
  
    private static class ExceptionLoggerHandler implements Thread.UncaughtExceptionHandler {
  
      @Override
      public void uncaughtException(Thread t, Throwable e) {
        Log.error("Exception occurred in thread :" + t.getName(), e);
        e.printStackTrace();
      }
    }
  
    private static class CUSTOM_FORMAT extends Formatter {
  
      @Override
      public String format(LogRecord record) {
        synchronized (this) {
          StringBuilder sb = new StringBuilder(levelShortStringMap.get(record.getLevel()));
          sb.append("|").append(format.format(new Date())).append("|");
          Object parameters[] = record.getParameters();
  
          boolean multiLine = false;
          if (record.getMessage().indexOf('\n') > 0) {
            multiLine = true;
          }
  
          // generate Exception String if available
          String throwStr = "";
          if (record.getThrown() != null) {
            Throwable t = record.getThrown();
            StringWriter sw = new StringWriter();
            try (PrintWriter pw = new PrintWriter(sw)) {
              t.printStackTrace(pw);
            }
            throwStr = sw.toString();
          }
  
          if (!multiLine) {
            if (parameters == null || parameters.length == 0) {
              sb.append(padIfNecessary(record.getMessage(), 100));
            } else {
              try {
                sb.append(
                    padIfNecessary(String.format(Locale.ENGLISH, record.getMessage(), parameters),
                        100));
              } catch (IllegalFormatException e) {
                sb.append("Log Format Error: ")
                    .append(record.getMessage())
                    .append(" With Parameters: ")
                    .append(Joiner.on(",").join(parameters));
              }
            }
            sb.append("| ")
                .append(Strings.subStringAfterLast(record.getSourceClassName(), "."))
                .append("#");
            sb.append(record.getSourceMethodName());
            sb.append("\n");
          } else {
            sb.append(" \u2193 |")
                .append(Strings.subStringAfterLast(record.getSourceClassName(), "."))
                .append("#");
            sb.append(record.getSourceMethodName());
            sb.append("\n");
            for (String s : Splitter.on("\n").split(record.getMessage())) {
              sb.append("    ").append(s).append("\n");
            }
          }
          if (throwStr.length() > 0) {
            sb.append("\n");
            for (String s : Splitter.on("\n").split(throwStr)) {
              sb.append(s).append("\n");
            }
          }
          return sb.toString();
        }
      }
    }
  
  }
  