Logger.groovy

/*
 * Copyright (C) 2003-2014 eXo Platform SAS.
 *
 * This file is part of eXo Platform - Add-ons Manager.
 *
 * eXo Platform - Add-ons Manager is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * eXo Platform - Add-ons Manager software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with eXo Platform - Add-ons Manager; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see <http://www.gnu.org/licenses/>.
 */
package org.exoplatform.platform.am.utils

import org.exoplatform.platform.am.AddonsManagerConstants
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiRenderer
import org.fusesource.jansi.AnsiString

/**
 * A really simple logger
 */
class Logger {

  /**
   * Singleton
   */
  private static final Logger singleton = new Logger()

  /**
   * Factory
   * @return The {@link Logger} singleton instance
   */
  static Logger getInstance() {
    return singleton
  }

  private final Console console

  /**
   * The enumeration of all possible log levels
   */
  enum Level {
    DEBUG(Ansi.Color.CYAN, "[DEBUG] "),
    INFO(Ansi.Color.DEFAULT, ""),
    WARN(Ansi.Color.MAGENTA, "[WARN]  "),
    ERROR(Ansi.Color.RED, "[ERROR] ")
    /**
     * Foreground color used for ascii supported console
     */
    final Ansi.Color color
    /**
     * Text prefix used for non-ascii supported console
     */
    final String prefix

    Level(Ansi.Color color, String prefix) {
      this.color = color
      this.prefix = prefix
    }
  }

  Logger() {
    this.console = Console.get()
  }

  Logger(Console console) {
    this.console = console
  }

  private boolean _debug

  boolean isDebugEnabled() {
    return _debug
  }

  void disableDebug() {
    _debug = false
  }

  void enableDebug() {
    _debug = true
    debug("Verbose logs activated")
  }

  void debug(final Object msg) {
    if (isDebugEnabled()) {
      log(Level.DEBUG, msg, null);
    }
  }

  void debug(final Object msg, final Throwable cause) {
    if (isDebugEnabled()) {
      log(Level.DEBUG, msg, cause);
    }
  }

  void debug(final String title, final Map map, final List<String> excludes) {
    if (isDebugEnabled()) {
      List<String> fieldsToExcludes = excludes ? excludes : []
      debugHR("=")
      debug("${title.toUpperCase()}:")
      debugHR()
      if (map) {
        map.keySet().findAll { !fieldsToExcludes.contains(it) }.each {
          debug String.format("%-${map.keySet()*.size().max()}s : %s", it, map.get(it))
        }
      } else {
        debug "Null"
      }
      debugHR("=")
    }
  }

  void debugHR(final String padding) {
    debug("".padRight(console.width - Level.DEBUG.prefix.length() - 1, padding))
  }

  void debugHR() {
    debugHR("-")
  }

  void info(final Object msg) {
    log(Level.INFO, msg, null);
  }

  void info(final Object msg, final Throwable cause) {
    log(Level.INFO, msg, cause);
  }

  void infoHR(final String padding) {
    info("".padRight(console.width - Level.INFO.prefix.length() - 1, padding))
  }

  void infoHR() {
    infoHR("-")
  }

  void warn(final Object msg) {
    log(Level.WARN, msg, null);
  }

  void warn(final Object msg, final Throwable cause) {
    log(Level.WARN, msg, cause);
  }

  void warnHR(final String padding) {
    warn("".padRight(console.width - Level.WARN.prefix.length() - 1, padding))
  }

  void warnHR() {
    warnHR("-")
  }

  void error(final Object msg) {
    log(Level.ERROR, msg, null);
  }

  void error(final Object msg, final Throwable cause) {
    log(Level.ERROR, msg, cause);
  }

  void errorHR(final String padding) {
    error("".padRight(console.width - Level.ERROR.prefix.length() - 1, padding))
  }

  void errorHR() {
    errorHR("-")
  }

  void displayHeader(String managerVersion) {
    info("""
    @|yellow               xx      xx |@
    @|yellow                xx    xx  |@
    @|yellow    eeeeeee      xx  xx  |@    ooooooo
    @|yellow  ee       ee     xxxx   |@  oo       @|yellow oo  |@
    @|yellow eeeeeeeeeeeee    xxxx   |@ oo        @|yellow  oo |@
    @|yellow ee              xx  xx  |@ oo        @|yellow  oo |@
    @|yellow  ee       ee   xx    xx |@  oo       @|yellow oo  |@
    @|yellow    eeeeeee    xx      xx    ooooooo |@      Add-ons Manager v@|yellow ${managerVersion} |@
    """)
  }

  /**
   * Breaks up long line into multiline at the word boundary
   * From : http://groovy.codehaus.org/Formatting+simple+tabular+text+data
   *
   * @param input long input line
   * @param lineWidth maximum output lines width
   *
   * @return multiline as an array of strings
   */
  List<String> wrapLine(input, lineWidth) {
    List<String> lines = []
    def line = ""
    def addWord;

    addWord = { word ->
      // Add new word if we have space in current line
      if ((line.size() + word.size()) <= lineWidth) {
        line <<= word
        if (line.size() < lineWidth)
          line <<= " "
        // Our word is longer than line width, break it up
      } else if (word.size() > lineWidth) {
        def len = lineWidth - line.length()
        line <<= word.substring(0, len)
        word = word.substring(len)
        lines += line.toString()

        while (word.size() > lineWidth) {
          lines += word.substring(0, lineWidth);
          word = word.substring(lineWidth);
        }
        line = word
        if (line.size() > 0 && line.size() < lineWidth)
          line <<= " "
        // No more space in line - wrap to another line
      } else {
        lines += line.toString()
        line = ""

        addWord(word)
      }
    }

    input.split(" ").each() {
      addWord(it)
    }

    lines += line.toString()

    return lines
  }

  def withStatus(String text, Closure closure, Object... args) {
    if (console.ansiSupported) {
      console.out.print text
    } else {
      console.out.print new AnsiString(AnsiRenderer.render(text)).plain
    }
    console.out.flush()
    try {
      def result = closure.call(args)
      displayStatus(text, AddonsManagerConstants.STATUS_OK, Ansi.Color.GREEN)
      return result
    } catch (Throwable t) {
      displayStatus(text, AddonsManagerConstants.STATUS_KO, Ansi.Color.RED)
      throw t
    }
  }

  void withStatusOK(String text) {
    if (console.ansiSupported) {
      console.out.print text
    } else {
      console.out.print new AnsiString(AnsiRenderer.render(text)).plain
    }
    displayStatus(text, AddonsManagerConstants.STATUS_OK, Ansi.Color.GREEN)
  }

  void withStatusKO(String text) {
    if (console.ansiSupported) {
      console.out.print text
    } else {
      console.out.print new AnsiString(AnsiRenderer.render(text)).plain
    }
    displayStatus(text, AddonsManagerConstants.STATUS_KO, Ansi.Color.RED)
  }

  private void log(final Level level, Object msg, Throwable cause) {
    assert level != null
    assert msg != null

    // Allow the msg to be a Throwable, and handle it properly if no cause is given
    if (cause == null) {
      if (msg instanceof Throwable) {
        cause = msg
        msg = msg.message
      }
    }

    if (console.ansiSupported) {
      if (Ansi.Color.DEFAULT != level.color) {
        console.out.println "@|${level.color.name()} ${level.prefix}${new AnsiString(AnsiRenderer.render(msg)).plain}|@"
      } else {
        console.out.println "${level.prefix}${AnsiRenderer.render(msg)}"
      }
    } else {
      console.out.println "${level.prefix}${new AnsiString(AnsiRenderer.render(msg)).plain}"
    }

    if (cause != null && isDebugEnabled()) {
      cause.printStackTrace(console.out);
    }
  }

  private void displayStatus(String text, String status, Ansi.Color color) {
    String statusStr = " [@|${color.name()} ${status.toUpperCase()}|@]"
    String padding = " ".padRight(console.width - new AnsiString(AnsiRenderer.render("${text}${statusStr}")).length() - 1, ".")
    if (console.ansiSupported) {
      console.out.println "${padding}${statusStr}"
    } else {
      console.out.println new AnsiString(AnsiRenderer.render("${padding}${statusStr}")).plain
    }

  }

}