AnsiMessagePrinter.java

  1. package ship.util;

  2. import static hera.util.StringUtils.removeSuffix;
  3. import static hera.util.ValidationUtils.assertEquals;
  4. import static hera.util.ValidationUtils.assertNotNull;
  5. import static hera.util.ValidationUtils.assertTrue;
  6. import static org.slf4j.LoggerFactory.getLogger;

  7. import java.io.IOException;
  8. import java.io.PrintStream;
  9. import java.io.StringReader;
  10. import java.io.StringWriter;
  11. import java.util.HashMap;
  12. import java.util.Map;
  13. import java.util.Stack;
  14. import lombok.Getter;
  15. import lombok.RequiredArgsConstructor;
  16. import lombok.Setter;
  17. import org.slf4j.Logger;

  18. public class AnsiMessagePrinter implements MessagePrinter {

  19.   public static final String COLOR_RESET   = "\u001B[0m";

  20.   public static final String COLOR_BLACK   = "\u001B[30m";
  21.   public static final String COLOR_RED     = "\u001B[31m";
  22.   public static final String COLOR_GREEN   = "\u001B[32m";
  23.   public static final String COLOR_YELLOW  = "\u001B[33m";
  24.   public static final String COLOR_BLUE    = "\u001B[34m";
  25.   public static final String COLOR_MAGENTA = "\u001B[35m";
  26.   public static final String COLOR_CYAN    = "\u001B[36m";
  27.   public static final String COLOR_WHITE   = "\u001B[37m";

  28.   public static final String COLOR_BRIGHT_BLACK    = removeSuffix(COLOR_BLACK, "m") + ";1m";
  29.   public static final String COLOR_BRIGHT_RED      = removeSuffix(COLOR_RED, "m") + ";1m";
  30.   public static final String COLOR_BRIGHT_GREEN    = removeSuffix(COLOR_GREEN, "m") + ";1m";
  31.   public static final String COLOR_BRIGHT_YELLOW   = removeSuffix(COLOR_YELLOW, "m") + ";1m";
  32.   public static final String COLOR_BRIGHT_BLUE     = removeSuffix(COLOR_BLUE, "m") + ";1m";
  33.   public static final String COLOR_BRIGHT_MAGENTA  = removeSuffix(COLOR_MAGENTA, "m") + ";1m";
  34.   public static final String COLOR_BRIGHT_CYAN     = removeSuffix(COLOR_CYAN, "m") + ";1m";
  35.   public static final String COLOR_BRIGHT_WHITE    = removeSuffix(COLOR_WHITE, "m") + ";1m";

  36.   public static final String BG_BLACK = "\u001B[40m";
  37.   public static final String BG_RED = "\u001B[41m";
  38.   public static final String BG_GREEN = "\u001B[42m";
  39.   public static final String BG_YELLOW = "\u001B[43m";
  40.   public static final String BG_BLUE = "\u001B[44m";
  41.   public static final String BG_MAGENTA = "\u001B[45m";
  42.   public static final String BG_CYAN = "\u001B[46m";
  43.   public static final String BG_WHITE = "\u001B[47m";

  44.   public static final String BG_BRIGHT_BLACK = removeSuffix(BG_BLACK, "m") + ";1m";
  45.   public static final String BG_BRIGHT_RED = removeSuffix(BG_RED, "m") + ";1m";
  46.   public static final String BG_BRIGHT_GREEN = removeSuffix(BG_GREEN, "m") + ";1m";
  47.   public static final String BG_BRIGHT_YELLOW = removeSuffix(BG_YELLOW, "m") + ";1m";
  48.   public static final String BG_BRIGHT_BLUE = removeSuffix(BG_BLUE, "m") + ";1m";
  49.   public static final String BG_BRIGHT_MAGENTA = removeSuffix(BG_MAGENTA, "m") + ";1m";
  50.   public static final String BG_BRIGHT_CYAN = removeSuffix(BG_CYAN, "m") + ";1m";
  51.   public static final String BG_BRIGHT_WHITE = removeSuffix(BG_WHITE, "m") + ";1m";

  52.   protected final transient Logger logger = getLogger(getClass());

  53.   @Getter
  54.   @Setter
  55.   protected String resetCode = COLOR_RESET;

  56.   @Getter
  57.   @Setter
  58.   protected Map<String, String> colors = new HashMap<>();

  59.   protected final PrintStream out;

  60.   /**
  61.    * Constructor with stream.
  62.    *
  63.    * @param out stream to print out
  64.    */
  65.   public AnsiMessagePrinter(final PrintStream out) {
  66.     this.out = out;
  67.     colors.put("black", COLOR_BLACK);
  68.     colors.put("red", COLOR_RED);
  69.     colors.put("green", COLOR_GREEN);
  70.     colors.put("yellow", COLOR_YELLOW);
  71.     colors.put("blue", COLOR_BLUE);
  72.     colors.put("magenta", COLOR_MAGENTA);
  73.     colors.put("cyan", COLOR_CYAN);
  74.     colors.put("white", COLOR_WHITE);

  75.     colors.put("bright_black", COLOR_BRIGHT_BLACK);
  76.     colors.put("bright_red", COLOR_BRIGHT_RED);
  77.     colors.put("bright_green", COLOR_BRIGHT_GREEN);
  78.     colors.put("bright_yellow", COLOR_BRIGHT_YELLOW);
  79.     colors.put("bright_blue", COLOR_BRIGHT_BLUE);
  80.     colors.put("bright_magenta", COLOR_BRIGHT_MAGENTA);
  81.     colors.put("bright_cyan", COLOR_BRIGHT_CYAN);
  82.     colors.put("bright_white", COLOR_BRIGHT_WHITE);

  83.     colors.put("bg_black", BG_BLACK);
  84.     colors.put("bg_red", BG_RED);
  85.     colors.put("bg_green", BG_GREEN);
  86.     colors.put("bg_yellow", BG_YELLOW);
  87.     colors.put("bg_blue", BG_BLUE);
  88.     colors.put("bg_magenta", BG_MAGENTA);
  89.     colors.put("bg_cyan", BG_CYAN);
  90.     colors.put("bg_white", BG_WHITE);

  91.     colors.put("bg_bright_black", BG_BRIGHT_BLACK);
  92.     colors.put("bg_bright_red", BG_BRIGHT_RED);
  93.     colors.put("bg_bright_green", BG_BRIGHT_GREEN);
  94.     colors.put("bg_bright_yellow", BG_BRIGHT_YELLOW);
  95.     colors.put("bg_bright_blue", BG_BRIGHT_BLUE);
  96.     colors.put("bg_bright_magenta", BG_BRIGHT_MAGENTA);
  97.     colors.put("bg_bright_cyan", BG_BRIGHT_CYAN);
  98.     colors.put("bg_bright_white", BG_BRIGHT_WHITE);
  99.   }

  100.   @RequiredArgsConstructor
  101.   abstract class State {
  102.     @Getter
  103.     protected final Stack<String> tags;

  104.     @Getter
  105.     protected final StringWriter writer;

  106.     abstract State next(int ch);
  107.   }

  108.   class Normal extends State {

  109.     protected static final int CH_TAG_START = '<';
  110.     protected static final int CH_ESCAPE = '!';

  111.     public Normal(final Stack<String> tags, final StringWriter writer) {
  112.       super(tags, writer);
  113.     }

  114.     @Override
  115.     public State next(int ch) {
  116.       if (CH_ESCAPE == ch) {
  117.         return new Escape(tags, writer);
  118.       } else if (CH_TAG_START == ch) {
  119.         return new TagOpen(tags, writer);
  120.       } else {
  121.         writer.write((char) ch);
  122.         return this;
  123.       }
  124.     }
  125.   }

  126.   class Escape extends State {

  127.     public Escape(final Stack<String> tags, final StringWriter writer) {
  128.       super(tags, writer);
  129.     }

  130.     @Override
  131.     public State next(int ch) {
  132.       writer.write((char) ch);
  133.       return new Normal(tags, writer);
  134.     }
  135.   }

  136.   class TagOpen extends State {
  137.     protected static final int CH_TAG_CLOSE = '/';

  138.     public TagOpen(final Stack<String> tags, final StringWriter writer) {
  139.       super(tags, writer);
  140.     }

  141.     @Override
  142.     public State next(int ch) {
  143.       if (ch == CH_TAG_CLOSE) {
  144.         return new CloseColor(tags, writer);
  145.       } else {
  146.         return new OpenColor(tags, writer).next(ch);
  147.       }
  148.     }
  149.   }

  150.   class OpenColor extends State {
  151.     protected static final int CH_TAG_END = '>';

  152.     protected final StringBuilder color = new StringBuilder();

  153.     public OpenColor(final Stack<String> tags, final StringWriter writer) {
  154.       super(tags, writer);
  155.     }

  156.     @Override
  157.     public State next(int ch) {
  158.       if (ch == CH_TAG_END) {
  159.         final String colorName = color.toString();
  160.         // check valid
  161.         tags.push(colorName);
  162.         logger.debug("Tags: {}", tags);
  163.         final String colorCode = colors.get(colorName);
  164.         assertNotNull(colorCode, "Unknown color: " + colorName);
  165.         writer.write(colorCode);
  166.         return new Normal(tags, writer);
  167.       } else {
  168.         color.append((char) ch);
  169.         return this;
  170.       }

  171.     }
  172.   }

  173.   class CloseColor extends State {

  174.     protected static final int CH_TAG_END = '>';

  175.     protected final StringBuilder color = new StringBuilder();

  176.     public CloseColor(final Stack<String> tags, final StringWriter writer) {
  177.       super(tags, writer);
  178.     }

  179.     @Override
  180.     public State next(int ch) {
  181.       if (ch == CH_TAG_END) {
  182.         final String colorName = color.toString();
  183.         // check valid
  184.         final String openColorName = tags.pop();
  185.         logger.debug("Tags: {}", tags);
  186.         assertEquals(openColorName, colorName,
  187.             "The closing tag not matched: " + openColorName + ", " + colorName);
  188.         color.delete(0, color.length());
  189.         if (tags.isEmpty()) {
  190.           writer.write(resetCode);
  191.         } else {
  192.           writer.write(colors.get(tags.peek()));
  193.         }
  194.         return new Normal(tags, writer);
  195.       } else {
  196.         color.append((char) ch);
  197.         return this;
  198.       }
  199.     }
  200.   }

  201.   /**
  202.    * Format tagged message with ansi color.
  203.    *
  204.    * @param message message to format
  205.    *
  206.    * @return text with ansi code
  207.    */
  208.   public String format(final String message) {
  209.     if (null == message) {
  210.       return null;
  211.     }
  212.     final StringReader reader = new StringReader(message);

  213.     int ch = 0;
  214.     State state = new Normal(new Stack<>(), new StringWriter());
  215.     try {
  216.       while (0 <= (ch = reader.read())) {
  217.         logger.trace("Char: {}", (char) ch);
  218.         final State old = state;
  219.         state = state.next(ch);
  220.         if (old != state) {
  221.           logger.debug("{} -> {}", old, state);
  222.         }
  223.       }
  224.     } catch (final IOException ex) {
  225.       throw new IllegalStateException();
  226.     }
  227.     logger.debug("State: {}", state);
  228.     logger.debug("Tags: {}", state.getTags());
  229.     assertTrue(state instanceof Normal, "Expression is invalid: " + state);
  230.     assertTrue(state.getTags().isEmpty(), "The closing tag missed: " + state.getTags());
  231.     return state.getWriter().toString();
  232.   }

  233.   public void print(final String format, Object...args) {
  234.     out.print(this.format(String.format(format, args)));
  235.   }

  236.   public void println() {
  237.     out.println();
  238.   }

  239.   public void println(final String format, Object... args) {
  240.     out.println(this.format(String.format(format, args)));
  241.   }

  242.   @Override
  243.   public void flush() {
  244.     out.flush();
  245.   }
  246. }