All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jline.console.impl.DefaultPrinter Maven / Gradle / Ivy

/*
 * Copyright (c) 2002-2021, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * https://opensource.org/licenses/BSD-3-Clause
 */
package org.jline.console.impl;

import java.io.File;
import java.math.BigDecimal;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.jline.builtins.ConfigurationPath;
import org.jline.builtins.Options;
import org.jline.builtins.Styles;
import org.jline.builtins.Nano.SyntaxHighlighter;
import org.jline.console.CmdDesc;
import org.jline.console.CommandInput;
import org.jline.console.Printer;
import org.jline.console.ScriptEngine;
import org.jline.console.SystemRegistry;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jline.utils.Log;
import org.jline.utils.StyleResolver;

/**
 * Print highlighted objects to console.
 *
 * @author Matti Rinta-Nikkola
 */
public class DefaultPrinter extends JlineCommandRegistry implements Printer {
    protected static final String VAR_PRNT_OPTIONS = "PRNT_OPTIONS";
    protected static final String VAR_NANORC = "NANORC";
    protected static final int PRNT_MAX_ROWS = 100000;
    protected static final int PRNT_MAX_DEPTH = 1;
    protected static final int PRNT_INDENTION = 4;
    private static final int NANORC_MAX_STRING_LENGTH = 400;

    private Map, Function>> objectToMap = new HashMap<>();
    private Map, Function> objectToString = new HashMap<>();
    private Map> highlightValue = new HashMap<>();
    private int totLines;

    private final ScriptEngine engine;
    private final ConfigurationPath configPath;
    private StyleResolver prntStyle;

    public DefaultPrinter(ConfigurationPath configPath) {
        this(null, configPath);
    }

    public DefaultPrinter(ScriptEngine engine, ConfigurationPath configPath) {
        this.engine = engine;
        this.configPath = configPath;
    }

    @Override
    public void println(Object object) {
        internalPrintln(defaultPrntOptions(false), object);
    }

    @Override
    public void println(Map optionsIn, Object object) {
        Map options = new HashMap<>(optionsIn);
        for (Map.Entry entry
                : defaultPrntOptions(options.containsKey(Printer.SKIP_DEFAULT_OPTIONS)).entrySet()) {
            options.putIfAbsent(entry.getKey(), entry.getValue());
        }
        manageBooleanOptions(options);
        internalPrintln(options, object);
    }

    public String[] appendUsage(String[] customUsage) {
        final String[] usage = {
                "prnt -  print object",
                "Usage: prnt [OPTIONS] object",
                "  -? --help                       Displays command help",
                "  -a --all                        Ignore columnsOut configuration",
                "  -b --border=CHAR                Table cell vertical border character",
                "  -c --columns=COLUMNS,...        Display given columns on map/table",
                "  -e --exclude=COLUMNS,...        Exclude given columns on table",
                "  -i --include=COLUMNS,...        Include given columns on table",
                "     --indention=INDENTION        Indention size",
                "     --maxColumnWidth=WIDTH       Maximum column width",
                "  -d --maxDepth=DEPTH             Maximum depth objects are resolved",
                "  -n --maxrows=ROWS               Maximum number of lines to display",
                "     --oneRowTable                Display one row data on table",
                "  -h --rowHighlight=ROW           Highlight table rows. ROW = EVEN, ODD, ALL",
                "  -r --rownum                     Display table row numbers",
                "     --shortNames                 Truncate table column names (property.field -> field)",
                "     --skipDefaultOptions         Ignore all options defined in PRNT_OPTIONS",
                "     --structsOnTable             Display structs and lists on table",
                "  -s --style=STYLE                Use nanorc STYLE to highlight Object.",
                "                                  STYLE = JSON serialize object to JSON string before printing",
                "     --toString                   Use object's toString() method to get print value",
                "                                  DEFAULT: object's fields are put to property map before printing",
                "     --valueStyle=STYLE           Use nanorc style to highlight string and column/map values",
                "  -w --width=WIDTH                Display width (default terminal width)"
        };
        String[] out;
        if (customUsage == null || customUsage.length == 0) {
            out = usage;
        } else {
            out = new String[usage.length + customUsage.length];
            System.arraycopy(usage, 0, out, 0, usage.length);
            System.arraycopy(customUsage, 0, out, usage.length, customUsage.length);
        }
        return out;
    }

    public Map compileOptions(Options opt) {
        Map options = new HashMap<>();
        if (opt.isSet(Printer.SKIP_DEFAULT_OPTIONS)) {
            options.put(Printer.SKIP_DEFAULT_OPTIONS, true);
        } else if (opt.isSet(Printer.STYLE)) {
            options.put(Printer.STYLE, opt.get(Printer.STYLE));
        }
        if (opt.isSet(Printer.TO_STRING)) {
            options.put(Printer.TO_STRING, true);
        }
        if (opt.isSet(Printer.WIDTH)) {
            options.put(Printer.WIDTH, opt.getNumber(Printer.WIDTH));
        }
        if (opt.isSet(Printer.ROWNUM)) {
            options.put(Printer.ROWNUM, true);
        }
        if (opt.isSet(Printer.ONE_ROW_TABLE)) {
            options.put(Printer.ONE_ROW_TABLE, true);
        }
        if (opt.isSet(Printer.SHORT_NAMES)) {
            options.put(Printer.SHORT_NAMES, true);
        }
        if (opt.isSet(Printer.STRUCT_ON_TABLE)) {
            options.put(Printer.STRUCT_ON_TABLE, true);
        }
        if (opt.isSet(Printer.COLUMNS)) {
            options.put(Printer.COLUMNS, Arrays.asList(opt.get(Printer.COLUMNS).split(",")));
        }
        if (opt.isSet(Printer.EXCLUDE)) {
            options.put(Printer.EXCLUDE, Arrays.asList(opt.get(Printer.EXCLUDE).split(",")));
        }
        if (opt.isSet(Printer.INCLUDE)) {
            options.put(Printer.INCLUDE, Arrays.asList(opt.get(Printer.INCLUDE).split(",")));
        }
        if (opt.isSet(Printer.ALL)) {
            options.put(Printer.ALL, true);
        }
        if (opt.isSet(Printer.MAXROWS)) {
            options.put(Printer.MAXROWS, opt.getNumber(Printer.MAXROWS));
        }
        if (opt.isSet(Printer.MAX_COLUMN_WIDTH)) {
            options.put(Printer.MAX_COLUMN_WIDTH, opt.getNumber(Printer.MAX_COLUMN_WIDTH));
        }
        if (opt.isSet(Printer.MAX_DEPTH)) {
            options.put(Printer.MAX_DEPTH, opt.getNumber(Printer.MAX_DEPTH));
        }
        if (opt.isSet(Printer.INDENTION)) {
            options.put(Printer.INDENTION, opt.getNumber(Printer.INDENTION));
        }
        if (opt.isSet(Printer.VALUE_STYLE)) {
            options.put(Printer.VALUE_STYLE, opt.get(Printer.VALUE_STYLE));
        }
        if (opt.isSet(Printer.BORDER)) {
            options.put(Printer.BORDER, opt.get(Printer.BORDER));
        }
        if (opt.isSet(Printer.ROW_HIGHLIGHT)) {
            try {
                options.put(Printer.ROW_HIGHLIGHT, optionRowHighlight(opt.get(Printer.ROW_HIGHLIGHT)));
            } catch (Exception e) {
                RuntimeException exception = new BadOptionValueException(Printer.ROW_HIGHLIGHT + " has a bad value: "
                        + opt.get(Printer.ROW_HIGHLIGHT));
                exception.addSuppressed(e);
                throw exception;
            }
        }
        options.put("exception", "stack");
        return options;
    }

    private TableRows optionRowHighlight(Object value) {
        if (value instanceof TableRows || value == null) {
            return (TableRows)value;
        } else if (value instanceof String) {
            String val = ((String)value).trim().toUpperCase();
            if (!val.isEmpty() && !val.equals("NULL")) {
                return TableRows.valueOf(val);
            } else {
                return null;
            }
        }
        throw new IllegalArgumentException("rowHighlight has a bad option value type: " + value.getClass());
    }

    @Override
    public Exception prntCommand(CommandInput input) {
        Exception out = null;
        String[] usage = appendUsage(null);
        try {
            Options opt = parseOptions(usage, input.xargs());
            Map options = compileOptions(opt);
            List args = opt.argObjects();
            if (args.size() > 0) {
                println(options, args.get(0));
            }
        } catch (Exception e) {
            out = e;
        }
        return out;
    }

    /**
     * Override ScriptEngine toMap() method
     * @param objectToMap key: object class, value: toMap function
     */
    public void setObjectToMap(Map, Function>> objectToMap) {
        this.objectToMap = objectToMap;
    }

    /**
     * Override ScriptEngine toString() method
     * @param objectToString key: object class, value: toString function
     */
    public void setObjectToString(Map, Function> objectToString) {
        this.objectToString = objectToString;
    }

    /**
     * Highlight column value
     * @param highlightValue key: regex for column name, value: highlight function
     */
    public void setHighlightValue(Map> highlightValue) {
        this.highlightValue = highlightValue;
    }

    /**
     *
     * @return terminal to which will be printed
     */
    protected Terminal terminal() {
        return SystemRegistry.get().terminal();
    }

    /**
     * Boolean printing options Printer checks only if key is present.
     * Boolean options that have false value are removed from the options Map.
     * @param options printing options
     */
    protected void manageBooleanOptions(Map options) {
        for (String key : Printer.BOOLEAN_KEYS) {
            if (options.containsKey(key)) {
                boolean value = options.get(key) instanceof Boolean && (boolean) options.get(key);
                if (!value) {
                    options.remove(key);
                }
            }
        }
    }

    /**
     * Set default and mandatory printing options.
     * Also unsupported options will be removed when Printer is used without scriptEngine
     * @param skipDefault when true does not set default options
     * @return default, mandatory and supported options
     */
    @SuppressWarnings("unchecked")
    protected Map defaultPrntOptions(boolean skipDefault) {
        Map out = new HashMap<>();
        if (engine != null && !skipDefault && engine.hasVariable(VAR_PRNT_OPTIONS)) {
            out.putAll((Map)engine.get(VAR_PRNT_OPTIONS));
            out.remove(Printer.SKIP_DEFAULT_OPTIONS);
            manageBooleanOptions(out);
        }
        out.putIfAbsent(Printer.MAXROWS, PRNT_MAX_ROWS);
        out.putIfAbsent(Printer.MAX_DEPTH, PRNT_MAX_DEPTH);
        out.putIfAbsent(Printer.INDENTION, PRNT_INDENTION);
        out.putIfAbsent(Printer.COLUMNS_OUT, new ArrayList());
        out.putIfAbsent(Printer.COLUMNS_IN, new ArrayList());
        if (engine == null) {
            out.remove(Printer.OBJECT_TO_MAP);
            out.remove(Printer.OBJECT_TO_STRING);
            out.remove(Printer.HIGHLIGHT_VALUE);
        }
        return out;
    }

    @SuppressWarnings("unchecked")
    private void internalPrintln(Map options, Object object) {
        if (object == null) {
            return;
        }
        long start = new Date().getTime();
        if (options.containsKey(Printer.EXCLUDE)) {
            List colOut = optionList(Printer.EXCLUDE, options);
            List colIn = optionList(Printer.COLUMNS_IN, options);
            colIn.removeAll(colOut);
            colOut.addAll((List)options.get(Printer.COLUMNS_OUT));
            options.put(Printer.COLUMNS_IN, colIn);
            options.put(Printer.COLUMNS_OUT, colOut);
        }
        if (options.containsKey(Printer.INCLUDE)) {
            List colIn = optionList(Printer.INCLUDE, options);
            colIn.addAll((List)options.get(Printer.COLUMNS_IN));
            options.put(Printer.COLUMNS_IN, colIn);
        }
        options.put(Printer.VALUE_STYLE, valueHighlighter((String)options.getOrDefault(Printer.VALUE_STYLE, null)));
        prntStyle = Styles.prntStyle();
        options.putIfAbsent(Printer.WIDTH, terminal().getSize().getColumns());
        String style = (String) options.getOrDefault(Printer.STYLE, "");
        options.put(Printer.STYLE, valueHighlighter(style));
        int width = (int) options.get(Printer.WIDTH);
        if (!style.isEmpty() && object instanceof String) {
            highlightAndPrint(width, (SyntaxHighlighter)options.get(Printer.STYLE), (String) object, true);
        } else if (style.equalsIgnoreCase("JSON")) {
            if (engine == null) {
                throw new IllegalArgumentException("JSON style not supported!");
            }
            String json = engine.toJson(object);
            highlightAndPrint(width, (SyntaxHighlighter)options.get(Printer.STYLE), json, true);
        } else if (options.containsKey(Printer.SKIP_DEFAULT_OPTIONS)) {
            highlightAndPrint(options, object);
        } else if (object instanceof Exception) {
            highlightAndPrint(options, (Exception)object);
        } else if (object instanceof CmdDesc) {
            highlight((CmdDesc)object).println(terminal());
        } else if (object instanceof String || object instanceof Number) {
            String str = object.toString();
            SyntaxHighlighter highlighter = (SyntaxHighlighter)options.getOrDefault(Printer.VALUE_STYLE, null);
            highlightAndPrint(width, highlighter, str, doValueHighlight(options, str));
        } else {
            highlightAndPrint(options, object);
        }
        terminal().flush();
        Log.debug("println: ", new Date().getTime() - start, " msec");
    }

    /**
     * Highlight and print an exception
     * @param options Printing options
     * @param exception Exception to be printed
     */
    protected void highlightAndPrint(Map options, Throwable exception) {
        SystemRegistry.get().trace(options.getOrDefault("exception", "stack").equals("stack"), exception);
    }

    private AttributedString highlight(CmdDesc cmdDesc) {
        StringBuilder sb = new StringBuilder();
        for (AttributedString as : cmdDesc.getMainDesc()) {
            sb.append(as.toString());
            sb.append("\n");
        }
        List tabs = Arrays.asList(0, 2, 33);
        for (Map.Entry> entry : cmdDesc.getOptsDesc().entrySet()) {
            AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.tabs(tabs);
            asb.append("\t");
            asb.append(entry.getKey());
            asb.append("\t");
            boolean first = true;
            for (AttributedString as : entry.getValue()) {
                if (!first) {
                    asb.append("\t");
                    asb.append("\t");
                }
                asb.append(as);
                asb.append("\n");
                first = false;
            }
            sb.append(asb.toString());
        }
        return Options.HelpException.highlight(sb.toString(), Styles.helpStyle());
    }

    private SyntaxHighlighter valueHighlighter(String style) {
        SyntaxHighlighter out;
        if (style == null || style.isEmpty()) {
            out = null;
        } else if (style.matches("[a-z]+:.*")) {
            out = SyntaxHighlighter.build(style);
        } else {
            Path nanorc = configPath != null ? configPath.getConfig("jnanorc") : null;
            if (engine != null && engine.hasVariable(VAR_NANORC)) {
                nanorc = Paths.get((String)engine.get(VAR_NANORC));
            }
            if (nanorc == null) {
                nanorc = Paths.get("/etc/nanorc");
            }
            out = SyntaxHighlighter.build(nanorc, style);
        }
        return out;
    }

    private String truncate4nanorc(String obj) {
        String val = obj;
        if (val.length() > NANORC_MAX_STRING_LENGTH && !val.contains("\n")) {
            val = val.substring(0, NANORC_MAX_STRING_LENGTH - 1);
        }
        return val;
    }

    private AttributedString highlight(Integer width, SyntaxHighlighter highlighter, String object, boolean doValueHighlight) {
        AttributedString out;
        AttributedStringBuilder asb = new AttributedStringBuilder();
        String val = object;
        if (highlighter != null && doValueHighlight) {
            val = truncate4nanorc(object);
        }
        asb.append(val);
        if (highlighter != null && val.length() < NANORC_MAX_STRING_LENGTH && doValueHighlight) {
            out = highlighter.highlight(asb);
        } else {
            out = asb.toAttributedString();
        }
        if (width != null) {
            out = out.columnSubSequence(0, width);
        }
        return out;
    }

    private boolean doValueHighlight(Map options, String value) {
        if (options.containsKey(Printer.VALUE_STYLE_ALL)
                || value.matches("\"(\\.|[^\"])*\"|'(\\.|[^'])*'")
                || (value.startsWith("[") && value.endsWith("]"))
                || (value.startsWith("(") && value.endsWith(")"))
                || (value.startsWith("{") && value.endsWith("}"))
                || (value.startsWith("<") && value.endsWith(">"))
           ) {
            return true;
        } else {
            return !value.contains(" ") && !value.contains("\t");
        }
    }

    private void highlightAndPrint(int width, SyntaxHighlighter highlighter, String object, boolean doValueHighlight) {
        for (String s: object.split("\\r?\\n")) {
            AttributedStringBuilder asb = new AttributedStringBuilder();
            List sas = asb.append(s).columnSplitLength(width);
            for (AttributedString as : sas) {
                highlight(width, highlighter, as.toString(), doValueHighlight).println(terminal());
            }
        }
    }

    private Map keysToString(Map map) {
        Map out = new HashMap<>();
        for (Map.Entry entry : map.entrySet()) {
            if (entry.getKey() instanceof String) {
                out.put((String)entry.getKey(), entry.getValue());
            } else if (entry.getKey() != null) {
                out.put(entry.getKey().toString(), entry.getValue());
            } else {
                out.put("null", entry.getValue());
            }
        }
        return out;
    }

    @SuppressWarnings("unchecked")
    private Object mapValue(Map options, String key, Map map) {
        Object out = null;
        if (map.containsKey(key)) {
            out = map.get(key);
        } else if (key.contains(".")) {
            String[] keys = key.split("\\.");
            out = map.get(keys[0]);
            for (int i = 1; i < keys.length; i++) {
                if (out instanceof Map) {
                    Map m = keysToString((Map) out);
                    out = m.get(keys[i]);
                } else if (canConvert(out)) {
                    out = engine.toMap(out).get(keys[i]);
                } else {
                    break;
                }
            }
        }
        if (!(out instanceof Map) && canConvert(out)){
            out = objectToMap(options, out);
        }
        return out;
    }

    @SuppressWarnings("unchecked")
    private List optionList(String key, Map options) {
        List out = new ArrayList<>();
        if (options.containsKey(key)) {
            if (options.get(key) instanceof String) {
                out.addAll(Arrays.asList(((String)options.get(key)).split(",")));
            } else if (options.get(key) instanceof Collection) {
                out.addAll((Collection)options.get(key));
            } else {
                throw new IllegalArgumentException("Unsupported option list: {key: " + key
                                                 + ", type: " + options.get(key).getClass() + "}");
            }
        }
        return out;
    }

    private boolean hasMatch(List regexes, String value) {
        for (String r: regexes) {
            if (value.matches(r)) {
                return true;
            }
        }
        return false;
    }

    private AttributedString addPadding(AttributedString str, int width) {
        AttributedStringBuilder sb = new AttributedStringBuilder();
        for (int i = str.columnLength(); i < width; i++) {
            sb.append(" ");
        }
        sb.append(str);
        return sb.toAttributedString();
    }

    private String addPadding(String str, int width) {
        AttributedStringBuilder sb = new AttributedStringBuilder();
        for (int i = str.length(); i < width; i++) {
            sb.append(" ");
        }
        sb.append(str);
        return sb.toString();
    }

    private String columnValue(String value) {
        return value.replaceAll("\r","CR").replaceAll("\n", "LF");
    }

    @SuppressWarnings("unchecked")
    private Map objectToMap(Map options, Object obj) {
        if (obj != null) {
            Map, Object> toMap = options.containsKey(Printer.OBJECT_TO_MAP)
                                                 ? (Map, Object>)options.get(Printer.OBJECT_TO_MAP)
                                                 : new HashMap<>();
            if (toMap.containsKey(obj.getClass())) {
                return (Map)engine.execute(toMap.get(obj.getClass()), obj);
            } else if (objectToMap.containsKey(obj.getClass())) {
                return objectToMap.get(obj.getClass()).apply(obj);
            }
        }
        return engine.toMap(obj);
    }

    @SuppressWarnings("unchecked")
    private String objectToString(Map options, Object obj) {
        String out = "null";
        if (obj != null) {
            Map, Object> toString = options.containsKey(Printer.OBJECT_TO_STRING)
                                                ? (Map, Object>)options.get(Printer.OBJECT_TO_STRING)
                                                : new HashMap<>();
            if (toString.containsKey(obj.getClass())) {
                out = (String) engine.execute(toString.get(obj.getClass()), obj);
            } else if (objectToString.containsKey(obj.getClass())) {
                out = objectToString.get(obj.getClass()).apply(obj);
            } else if (obj instanceof Class) {
                out = ((Class) obj).getName();
            } else if (engine != null) {
                out = engine.toString(obj);
            } else {
                out = obj.toString();
            }
        }
        return out;
    }

    private AttributedString highlightMapValue(Map options, String key, Map map) {
        return highlightValue(options, key, mapValue(options, key, map));
    }

    private boolean isHighlighted(AttributedString value) {
        for (int i = 0; i < value.length(); i++) {
            if (value.styleAt(i).getStyle() != AttributedStyle.DEFAULT.getStyle()) {
                return true;
            }
        }
        return false;
    }

    @SuppressWarnings("unchecked")
    private AttributedString highlightValue(Map options, String column, Object obj) {
        AttributedString out = null;
        Object raw = options.containsKey(Printer.TO_STRING) && obj != null ? objectToString(options, obj) : obj;
        Map hv = options.containsKey(Printer.HIGHLIGHT_VALUE)
                ? (Map) options.get(Printer.HIGHLIGHT_VALUE)
                : new HashMap<>();
        if (column != null && simpleObject(raw)) {
            for (Map.Entry entry : hv.entrySet()) {
                if (!entry.getKey().equals("*") && column.matches(entry.getKey())) {
                    out = (AttributedString) engine.execute(hv.get(entry.getKey()), raw);
                    break;
                }
            }
            if (out == null) {
                for (Map.Entry> entry : highlightValue.entrySet()) {
                    if (!entry.getKey().equals("*") && column.matches(entry.getKey())) {
                        out = highlightValue.get(entry.getKey()).apply(raw);
                        break;
                    }
                }
            }
        }
        if (out == null) {
            if (raw instanceof String) {
                out = new AttributedString(columnValue((String)raw));
            } else {
                out = new AttributedString(columnValue(objectToString(options, raw)));
            }
       }
       if ((simpleObject(raw) || raw == null) && (hv.containsKey("*") || highlightValue.containsKey("*"))
                && !isHighlighted(out)) {
            if (hv.containsKey("*")) {
                out = (AttributedString) engine.execute(hv.get("*"), out);
            }
            if (highlightValue.containsKey("*")) {
                out = highlightValue.get("*").apply(out);
            }
        }
        if (options.containsKey(Printer.VALUE_STYLE) && !isHighlighted(out)) {
            out = highlight(null, (SyntaxHighlighter)options.get(Printer.VALUE_STYLE), out.toString()
                    , doValueHighlight(options, out.toString()));
        }
        return truncateValue(options, out);
    }

    private AttributedString truncateValue(Map options, AttributedString value) {
        if (value.columnLength() > (int)options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE)) {
            AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.append(value.columnSubSequence(0, (int)options.get(Printer.MAX_COLUMN_WIDTH) - 3));
            asb.append("...");
            return asb.toAttributedString();
        }
        return value;
    }

    private String truncateValue(int maxWidth, String value) {
        if (value.length() > maxWidth) {
            return value.subSequence(0, maxWidth - 3) + "...";
        }
        return value;
    }

    @SuppressWarnings("unchecked")
    private List objectToList(Object obj) {
        List out = new ArrayList<>();
        if (obj instanceof List) {
            out = (List)obj;
        } else if (obj instanceof Collection) {
            out.addAll((Collection) obj);
        } else if (obj instanceof Object[]) {
            out.addAll(Arrays.asList((Object[]) obj));
        } else if (obj instanceof Iterator) {
            ((Iterator) obj).forEachRemaining(out::add);
        } else if (obj instanceof Iterable) {
            ((Iterable) obj).forEach(out::add);
        } else {
            out.add(obj);
        }
        return out;
    }

    private boolean similarSets(final List ref, final Set c2, final int matchLimit) {
        boolean out = false;
        int limit = matchLimit;
        for (String s : ref) {
            if (c2.contains(s)) {
                limit--;
                if (limit == 0) {
                    out = true;
                    break;
                }
            }
        }
        return out;
    }

    @SuppressWarnings("serial")
    private static class BadOptionValueException extends RuntimeException {
        public BadOptionValueException(String message) {
            super(message);
        }
    }

    @SuppressWarnings("serial")
    private static class TruncatedOutputException extends RuntimeException {
        public TruncatedOutputException(String message) {
            super(message);
        }
    }

    private void println(AttributedString line, int maxrows) {
        line.println(terminal());
        totLines++;
        if (totLines > maxrows) {
            totLines = 0;
            throw new TruncatedOutputException("Truncated output: " + maxrows);
        }
    }

    private String columnName(String name, boolean shortName) {
        String out = name;
        if (shortName) {
            String[] p = name.split("\\.");
            out = p[p.length - 1];
        }
        return out;
    }

    private boolean isNumber(String str) {
        return str.matches("-?\\d+(\\.\\d+)?");
    }

    @SuppressWarnings("unchecked")
    private void highlightAndPrint(Map options, Object obj) {
        int width = (int)options.get(Printer.WIDTH);
        totLines = 0;
        String message = null;
        RuntimeException runtimeException = null;
        if (obj == null) {
            // do nothing
        } else if (obj instanceof Map) {
            highlightMap(options, keysToString((Map)obj), width);
        } else if (collectionObject(obj)) {
            List collection = objectToList(obj);
            if (collection.size() > (int)options.get(Printer.MAXROWS)) {
                message = "Truncated output: " + options.get(Printer.MAXROWS) + "/" + collection.size();
                collection = collection.subList(collection.size() - (int)options.get(Printer.MAXROWS), collection.size());
            }
            if (!collection.isEmpty()) {
                if (collection.size() == 1 && !options.containsKey(Printer.ONE_ROW_TABLE)) {
                    Object elem = collection.iterator().next();
                    if (elem instanceof Map) {
                        highlightMap(options, keysToString((Map)elem), width);
                    } else if (canConvert(elem) && !options.containsKey(Printer.TO_STRING)) {
                        highlightMap(options, objectToMap(options, elem), width);
                    } else if (elem instanceof String && options.get(Printer.STYLE) != null) {
                        highlightAndPrint(width, (SyntaxHighlighter)options.get(Printer.STYLE), (String)elem, true);
                    } else {
                        highlightValue(options, null, objectToString(options, obj)).println(terminal());
                    }
                } else {
                    String columnSep = "";
                    TableRows tableRows = null;
                    boolean rownum = options.containsKey(Printer.ROWNUM);
                    try {
                        columnSep = (String) options.getOrDefault(Printer.BORDER, "");
                        tableRows = optionRowHighlight(options.getOrDefault(Printer.ROW_HIGHLIGHT, null));
                    } catch (Exception e) {
                        runtimeException = new BadOptionValueException("Option " + Printer.BORDER + " or "
                                + Printer.ROW_HIGHLIGHT + " has a bad value!");
                        runtimeException.addSuppressed(e);
                    }
                    try {
                        Object elem = collection.iterator().next();
                        boolean convert = canConvert(elem);
                        if ((elem instanceof Map || convert) && !options.containsKey(Printer.TO_STRING)) {
                            List> convertedCollection = new ArrayList<>();
                            Set keys = new HashSet<>();
                            for (Object o : collection) {
                                Map m = convert ? objectToMap(options, o)
                                        : keysToString((Map) o);
                                convertedCollection.add(m);
                                keys.addAll(m.keySet());
                            }
                            List _header;
                            List columnsIn = optionList(Printer.COLUMNS_IN, options);
                            List columnsOut = !options.containsKey("all") ? optionList(Printer.COLUMNS_OUT, options)
                                                                                  : new ArrayList<>();
                            if (options.containsKey(Printer.COLUMNS)) {
                                _header = (List) options.get(Printer.COLUMNS);
                            } else {
                                _header = columnsIn;
                                _header.addAll(keys.stream()
                                        .filter(k -> !columnsIn.contains(k) && !hasMatch(columnsOut, k))
                                        .collect(Collectors.toList()));
                            }
                            List header = new ArrayList<>();
                            List columns = new ArrayList<>();
                            int headerWidth = 0;
                            List refKeys = new ArrayList<>();
                            for (String v : _header) {
                                String value = v.split("\\.")[0];
                                if (!keys.contains(value) && !keys.contains(v)) {
                                    continue;
                                }
                                boolean addKey = false;
                                for (Map m : convertedCollection) {
                                    Object val = mapValue(options, v, m);
                                    if (val != null) {
                                        addKey = simpleObject(val) || options.containsKey(Printer.COLUMNS)
                                                || options.containsKey(Printer.STRUCT_ON_TABLE);
                                        break;
                                    }
                                }
                                if (!addKey) {
                                    continue;
                                }
                                refKeys.add(value);
                                header.add(v);
                                String cn = columnName(v, options.containsKey(Printer.SHORT_NAMES));
                                columns.add(cn.length() + 1);
                                headerWidth += cn.length() + 1;
                                if (headerWidth > width) {
                                    break;
                                }
                            }
                            if (header.size() == 0) {
                                throw new Exception("No columns for table!");
                            }
                            double mapSimilarity = ((BigDecimal)options.getOrDefault(Printer.MAP_SIMILARITY
                                    , new BigDecimal("0.8"))).doubleValue();
                            int matchLimit = (int)Math.ceil(header.size() * mapSimilarity);
                            for (Map m : convertedCollection) {
                                if (!similarSets(refKeys, m.keySet(), matchLimit)) {
                                    throw new Exception("Not homogenous list!");
                                }
                                for (int i = 0; i < header.size(); i++) {
                                    int cw = highlightMapValue(options, header.get(i), m).columnLength();
                                    if (cw > columns.get(i) - 1) {
                                        columns.set(i, cw + 1);
                                    }
                                }
                            }
                            toTabStops(columns, collection.size(), rownum, columnSep);
                            AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns);
                            asb.style(prntStyle.resolve(".th"));
                            int firstColumn = 0;
                            if (rownum) {
                                asb.append(addPadding("", columns.get(0) - columnSep.length() - 1));
                                asb.append(columnSep);
                                asb.append("\t");
                                firstColumn = 1;
                            }
                            boolean first = true;
                            for (String s : header) {
                                if (!first) {
                                    asb.append(columnSep);
                                }
                                asb.append(columnName(s, options.containsKey(Printer.SHORT_NAMES)));
                                asb.append("\t");
                                first = false;
                            }
                            asb.columnSubSequence(0, width).println(terminal());
                            int row = 0;
                            for (Map m : convertedCollection) {
                                AttributedStringBuilder asb2 = new AttributedStringBuilder().tabs(columns);
                                if (doRowHighlight(row, tableRows)) {
                                    asb2.style(prntStyle.resolve(".rs"));
                                }
                                if (rownum) {
                                    asb2.styled(prntStyle.resolve(".rn")
                                            , addPadding(Integer.toString(row), columns.get(0) - columnSep.length() - 1));
                                    asb2.append(columnSep);
                                    asb2.append("\t");
                                }
                                row++;
                                for (int i = 0; i < header.size(); i++) {
                                    if (i > 0) {
                                        asb2.append(columnSep);
                                    }
                                    AttributedString v = highlightMapValue(options, header.get(i), m);
                                    if (isNumber(v.toString())) {
                                        v = addPadding(v, cellWidth(firstColumn + i, columns, rownum, columnSep) - 1);
                                    }
                                    asb2.append(v);
                                    asb2.append("\t");
                                }
                                asb2.columnSubSequence(0, width).println(terminal());
                            }
                        } else if (collectionObject(elem) && !options.containsKey(Printer.TO_STRING)) {
                            List columns = new ArrayList<>();
                            for (Object o : collection) {
                                List inner = objectToList(o);
                                for (int i = 0; i < inner.size(); i++) {
                                    int len1 = objectToString(options, inner.get(i)).length() + 1;
                                    if (columns.size() <= i) {
                                        columns.add(len1);
                                    } else if (len1 > columns.get(i)) {
                                        columns.set(i, len1);
                                    }
                                }
                            }
                            toTabStops(columns, collection.size(), rownum, columnSep);
                            int row = 0;
                            int firstColumn = rownum ? 1 : 0;
                            for (Object o : collection) {
                                AttributedStringBuilder asb = new AttributedStringBuilder().tabs(columns);
                                if (doRowHighlight(row, tableRows)) {
                                    asb.style(prntStyle.resolve(".rs"));
                                }
                                if (rownum) {
                                    asb.styled(prntStyle.resolve(".rn")
                                            , addPadding(Integer.toString(row), columns.get(0) - columnSep.length() - 1));
                                    asb.append(columnSep);
                                    asb.append("\t");
                                }
                                row++;
                                List inner = objectToList(o);
                                for (int i = 0; i < inner.size(); i++) {
                                    if (i > 0) {
                                        asb.append(columnSep);
                                    }
                                    AttributedString v = highlightValue(options, null, inner.get(i));
                                    if (isNumber(v.toString())) {
                                        v = addPadding(v, cellWidth(firstColumn + i, columns, rownum, columnSep) - 1);
                                    }
                                    asb.append(v);
                                    asb.append("\t");
                                }
                                asb.columnSubSequence(0, width).println(terminal());
                            }
                        } else {
                            highlightList(options, collection, width);
                        }
                    } catch (Exception e) {
                        Log.debug("Stack: ", e);
                        highlightList(options, collection, width);
                    }
                }
            } else {
                highlightValue(options, null, objectToString(options, obj)).println(terminal());
            }
        } else if (canConvert(obj) && !options.containsKey(Printer.TO_STRING)) {
            highlightMap(options, objectToMap(options, obj), width);
        } else {
            highlightValue(options, null, objectToString(options, obj)).println(terminal());
        }
        if (message != null) {
            AttributedStringBuilder asb = new AttributedStringBuilder();
            asb.styled(prntStyle.resolve(".em"), message);
            asb.println(terminal());
        }
        if (runtimeException != null) {
            throw runtimeException;
        }
    }

    private boolean doRowHighlight(int row, TableRows tableRows) {
        if (tableRows == null) {
            return false;
        }
        switch (tableRows) {
            case EVEN:
                return row % 2 == 0;
            case ODD:
                return row % 2 == 1;
            case ALL:
                return true;
        }
        return false;
    }

    private void highlightList(Map options
            , List collection, int width) {
        highlightList(options, collection, width, 0);
    }

    private void highlightList(Map options
                            , List collection, int width, int depth) {
        int row = 0;
        int maxrows = (int)options.get(Printer.MAXROWS);
        int indent = (int)options.get(Printer.INDENTION);
        List tabs = new ArrayList<>();
        tabs.add(indent*depth);
        if (options.containsKey(Printer.ROWNUM)) {
            tabs.add(indent*depth + digits(collection.size()) + 2);
        }
        options.remove(Printer.MAX_COLUMN_WIDTH);
        SyntaxHighlighter highlighter = depth == 0 ? (SyntaxHighlighter)options.get(Printer.STYLE) : null;
        for (Object o : collection) {
            AttributedStringBuilder asb = new AttributedStringBuilder().tabs(tabs);
            if (depth > 0) {
                asb.append("\t");
            }
            if (options.containsKey(Printer.ROWNUM)) {
                asb.styled(prntStyle.resolve(".rn"), Integer.toString(row)).append(":");
                asb.append("\t");
                row++;
            }
            if (highlighter != null && o instanceof String) {
                asb.append(highlighter.highlight((String)o));
            } else {
                asb.append(highlightValue(options, null, o));
            }
            println(asb.columnSubSequence(0, width), maxrows);
        }
    }

    private boolean collectionObject(Object obj) {
        return obj instanceof Iterator || obj instanceof Iterable || obj instanceof Object[];
    }

    private boolean simpleObject(Object obj) {
        return obj instanceof Number || obj instanceof String || obj instanceof Date || obj instanceof File
                || obj instanceof Boolean || obj instanceof Enum;
    }

    private boolean canConvert(Object obj) {
        return engine != null && obj != null && !(obj instanceof Class) && !(obj instanceof Map) && !simpleObject(obj)
                && !collectionObject(obj);
    }

    private int digits(int number) {
        if (number < 100) {
            return number < 10 ? 1 : 2;
        } else if (number < 1000) {
            return 3;
        } else {
            return number < 10000 ? 4 : 5;
        }
    }

    private int cellWidth(int pos, List columns, boolean rownum, String columnSep) {
        if (pos == 0) {
            return columns.get(0);
        }
        return columns.get(pos) - columns.get(pos - 1)
                   - (rownum && pos == 1 ? 0 : columnSep.length());
    }

    private void toTabStops(List columns, int rows, boolean rownum, String columnSep) {
        if (rownum) {
            columns.add(0, digits(rows) + 2 + columnSep.length());
        }
        for (int i = 1; i < columns.size(); i++) {
            columns.set(i, columns.get(i - 1) + columns.get(i) + (i > 1 || !rownum ? columnSep.length() : 0));
        }
    }

    private void highlightMap(Map options, Map map, int width) {
        if (!map.isEmpty()) {
            highlightMap(options, map, width, 0);
        } else {
            highlightValue(options, null, objectToString(options, map)).println(terminal());
        }
    }

    @SuppressWarnings("unchecked")
    private void highlightMap(Map options
                            , Map map, int width, int depth) {
        int maxrows = (int)options.get(Printer.MAXROWS);
        int max = map.keySet().stream().map(String::length).max(Integer::compareTo).get();
        if (max > (int)options.getOrDefault(Printer.MAX_COLUMN_WIDTH, Integer.MAX_VALUE)) {
            max = (int)options.get(Printer.MAX_COLUMN_WIDTH);
        }
        Map mapOptions = new HashMap<>(options);
        mapOptions.remove(Printer.MAX_COLUMN_WIDTH);
        int indent = (int)options.get(Printer.INDENTION);
        int maxDepth = (int)options.get(Printer.MAX_DEPTH);
        for (Map.Entry entry : map.entrySet()) {
            if (depth == 0 && options.containsKey(Printer.COLUMNS)
                    && !((List)options.get(Printer.COLUMNS)).contains(entry.getKey())) {
                continue;
            }
            AttributedStringBuilder asb = new AttributedStringBuilder().tabs(Arrays.asList(0, depth*indent, depth*indent + max + 1));
            if (depth != 0) {
                asb.append("\t");
            }
            asb.styled(prntStyle.resolve(".mk"), truncateValue(max, entry.getKey()));
            Object elem = entry.getValue();
            boolean convert = canConvert(elem);
            boolean highlightValue = true;
            if (depth < maxDepth && !options.containsKey(Printer.TO_STRING)) {
                if (elem instanceof Map || convert) {
                    Map childMap = convert ? objectToMap(options, elem)
                                                           : keysToString((Map) elem);
                    if (!childMap.isEmpty()) {
                        println(asb.columnSubSequence(0, width), maxrows);
                        highlightMap(options, childMap, width, depth + 1);
                        highlightValue = false;
                    }
                } else if (collectionObject(elem)) {
                    List collection = objectToList(elem);
                    if (!collection.isEmpty()) {
                        println(asb.columnSubSequence(0, width), maxrows);
                        Map listOptions = new HashMap<>(options);
                        listOptions.put(Printer.TO_STRING, true);
                        highlightList(listOptions, collection, width, depth + 1);
                        highlightValue = false;
                    }
                }
            }
            if (highlightValue) {
                AttributedString val = highlightMapValue(mapOptions, entry.getKey(), map);
                asb.append("\t");
                if (map.size() == 1) {
                    if (val.contains('\n')) {
                        for (String v : val.toString().split("\\r?\\n")) {
                            asb.append(highlightValue(options, entry.getKey(), v));
                            println(asb.columnSubSequence(0, width), maxrows);
                            asb = new AttributedStringBuilder().tabs(Arrays.asList(0, max + 1));
                        }
                    } else {
                        asb.append(val);
                        println(asb.columnSubSequence(0, width), maxrows);
                    }
                } else {
                    if (val.contains('\n')) {
                        val = new AttributedString(Arrays.asList(val.toString().split("\\r?\\n")).toString());
                        asb.append(highlightValue(options, entry.getKey(), val.toString()));
                    } else {
                        asb.append(val);
                    }
                    println(asb.columnSubSequence(0, width), maxrows);
                }
            }
        }
    }
}