IndentedPrintWriter.java

/*
 * Copyright (c) 2016, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.morimekta.strings.io;

import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Locale;
import java.util.Stack;

/**
 * Print writer that prints lines with indent. In order to have full control
 * over the lines line-separators will be pre-pended the println calls, not
 * appended. This is a break with the standard convention, but then the calls
 * to println, appendln etc will always be consistent with the next
 * line-length.
 * <p>
 * Example usage:
 * <pre>{@code
 *     IndentedPrintWriter pw = new IndentedPrintWriter(out);
 *     pw.append("Start:");
 *     pw.begin();
 *     pw.println(144);
 *     pw.println(12.77d);
 *     pw.end();
 *     pw.appendln("end.");
 *     }</pre>
 * <p>
 * Will print the following:
 * <pre>{@code
 *     Start:
 *         144
 *         12.77
 *     end.
 *     }</pre>
 * <p>
 * The print writer also has support for writing multiline strings into it's
 * indented structure. E.g. (assuming java 14 string blocks):
 * <pre>{@code
 *     IndentedPrintWriter pw = new IndentedPrintWriter(out, "  ", "\n");
 *     pw.append("container:);
 *     pw.begin()
 *     pw.appendln("""
 *                 foo:
 *                    bar: bar
 *                    list:
 *                    - entry
 *                    - entry2
 *                 """);
 *     wp.end();
 *     }</pre>
 * <p>
 * This should print the following:
 * <pre>{@code
 *     container:
 *       foo:
 *         bar: bar
 *         list:
 *         - entry
 *         - entry2
 *     }</pre>
 * <p>
 * Note that since the writer assumes to be at the end of last line, and
 * will start a new line just <i>before</i> writing a new "line", the first
 * line's content must be written with a call to {@link #append(CharSequence)}
 * not to {@link #appendln(CharSequence)}.
 */
public class IndentedPrintWriter extends PrintWriter {
    /**
     * The default newline string.
     */
    public final static String NEWLINE = "\n";
    /**
     * The default indent string.
     */
    public final static String INDENT  = "    ";

    private final Stack<String> indents;
    private final String        indent;
    private final String        newline;
    private       String        current;

    /**
     * Make indented print writer with default indent and newline.
     *
     * @param out Stream to write to.
     */
    public IndentedPrintWriter(OutputStream out) {
        this(out, INDENT, NEWLINE);
    }

    /**
     * Make indented print writer.
     *
     * @param out     Stream to write to.
     * @param indent  Default indent.
     * @param newline Newline character used.
     */
    public IndentedPrintWriter(OutputStream out, String indent, String newline) {
        this(new Utf8StreamWriter(out), indent, newline);
    }

    /**
     * Make indented print writer with default indent and newline.
     *
     * @param out Writer to output to.
     */
    public IndentedPrintWriter(Writer out) {
        this(out, INDENT, NEWLINE);
    }

    /**
     * Make indented print writer.
     *
     * @param out     Writer to output to.
     * @param indent  Default indent.
     * @param newline Newline character used.
     */
    public IndentedPrintWriter(Writer out, String indent, String newline) {
        super(out);
        this.indent = indent;
        this.newline = newline;

        this.indents = new Stack<>();
        this.current = "";
    }

    /**
     * Begin a new indent block.
     *
     * @return The writer.
     */
    public IndentedPrintWriter begin() {
        return begin(indent);
    }

    /**
     * Begin a new indent block.
     *
     * @param indent Indent to use instead of default.
     * @return The writer.
     */
    public IndentedPrintWriter begin(String indent) {
        indents.push(current);
        current = current + indent;
        return this;
    }

    /**
     * End an indent block.
     *
     * @return The writer.
     */
    public IndentedPrintWriter end() {
        if (indents.isEmpty()) {
            throw new IllegalStateException("No indent to end");
        }
        current = indents.pop();
        return this;
    }

    /**
     * Create a new-line char, no indent after it.
     *
     * @return The writer.
     */
    public IndentedPrintWriter newline() {
        return append(newline);
    }

    /**
     * Append a new line and prepare indent on new.
     *
     * @return The writer.
     */
    public IndentedPrintWriter appendln() {
        newline();
        super.append(current);
        return this;
    }

    /**
     * Append a new line, indent and write char.
     *
     * @param c Char to write.
     * @return The writer.
     */
    public IndentedPrintWriter appendln(char c) {
        appendln();
        super.append(c);
        return this;
    }

    /**
     * Append a new line, indent and write string.
     *
     * @param str String to write. If the string is multiline, it will indent all the
     *            lines with the current indent. Empty lines will not be indented.
     * @return The writer.
     */
    public IndentedPrintWriter appendln(CharSequence str) {
        String[] lines = str.toString().split("[\\n]");
        for (String line : lines) {
            if (line.isBlank()) {
                newline();
            } else {
                appendln();
                print(line);
            }
        }
        return this;
    }

    /**
     * Append a new line, indent and write a formatted string.
     *
     * @param format String format. If the resulting string is multiline, it will
     *               indent all the lines with the current indent. Empty lines will not be indented.
     * @param args   Args used to format line.
     * @return The writer.
     */
    public IndentedPrintWriter formatln(String format, Object... args) {
        return appendln(String.format(format, args));
    }

    // --- Override PrintWriter methods to return IndentedPrintWriter.

    @Override
    public IndentedPrintWriter printf(String format, Object... args) {
        super.format(format, args);
        return this;
    }

    @Override
    public IndentedPrintWriter printf(Locale l, String format, Object... args) {
        super.format(l, format, args);
        return this;
    }

    @Override
    public IndentedPrintWriter format(String format, Object... args) {
        super.format(format, args);
        return this;
    }

    @Override
    public IndentedPrintWriter format(Locale l, String format, Object... args) {
        super.format(l, format, args);
        return this;
    }

    @Override
    public IndentedPrintWriter append(CharSequence str) {
        super.append(str);
        return this;
    }

    @Override
    public IndentedPrintWriter append(CharSequence str, int start, int end) {
        super.append(str, start, end);
        return this;
    }

    @Override
    public IndentedPrintWriter append(char c) {
        super.append(c);
        return this;
    }

    // --- Override PrintWriter methods to work like IndentedPrintWriter.

    @Override
    public void println() {
        newline();
    }

    @Override
    public void println(boolean x) {
        appendln().print(x);
    }

    @Override
    public void println(char x) {
        appendln().print(x);
    }

    @Override
    public void println(int x) {
        appendln().print(x);
    }

    @Override
    public void println(long x) {
        appendln().print(x);
    }

    @Override
    public void println(float x) {
        appendln().print(x);
    }

    @Override
    public void println(double x) {
        appendln().print(x);
    }

    @Override
    public void println(char[] x) {
        appendln(new String(x));
    }

    @Override
    public void println(String x) {
        appendln(x);
    }

    @Override
    public void println(Object x) {
        appendln(String.valueOf(x));
    }
}