IndentWriter

おそらく他の人には使い道の想像がつかないのではないかと思う(笑)

以前、コンソールアプリケーションを作ったことがある。
いわゆるバッチ的なものではなく、ユーザーの入力に応答する類のもの。

当然いくつかコマンドがあり、そのコマンドのヘルプも表示する必要があった。
コマンドプロンプトのヘルプをいくつか見るとわかるが、コンソールにヘルプを表示すると、ある程度長い文字列は途中で改行し、なおかつ改行の後に決まった数の空白を追加しなければいけない。
要するに、それを自動化するためのクラスである。

import java.io.FilterWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.Arrays;

/**
 * 行頭に指定された数の空白を出力するライタ。
 * 改行以外の制御文字(\b、\fなど)は無視する。
 * \tは空白として出力される。
 * 結合文字を使用すると文字数のカウントが正確には出来ない。
 */
public class IndentWriter extends FilterWriter {

    // 改行コード
    private static String LINE_SEPARATOR = System.getProperty("line.separator");
    // 改行コードの最後の文字
    private static char LAST_LINE_SEPARATOR_CHAR = LINE_SEPARATOR.charAt(LINE_SEPARATOR.length() - 1);

    // 行内での出力位置
    private int currentPos = 0;
    // インデントの数
    private int indentLevel = 0;
    // インデントごとのスペースの数
    private int indentUnit;
    // インデントとして使用する文字列
    private String indentString = "";
    // 文字数の計算に使用する文字コード
    private Charset cs;
    // 1行あたりの最大文字数
    private int lineLength;
    // タブの文字数
    private int tabLength = 8;
    // インデント全体の文字数
    private int indentCount;

    /**
     * indentUnit = 2, lineLength = 0, tabLength = 8, cs = nullでインスタンスを作成する
     * @param writer
     */
    public IndentWriter(Writer writer) {
        this(writer, 2, 0, 8, null);
    }

    /**
     * Windowsのコマンドプロンプトは1行80文字だが、80を指定すると画面上では余計な改行が入る。<br/>
     * ファイルにリダイレクトなどすると正しく出力できているのだが・・・<br/>
     * @param writer
     * @param indentUnit 1以上lineLength以下(lineLength > 0の場合のみ)
     * @param lineLength 0以下を指定すると文字数制限なし
     * @param tabLength \tの代わりに出力する空白の数
     * @param cs nullを指定するとデフォルトの文字コード
     */
    public IndentWriter(Writer writer, int indentUnit, int lineLength, int tabLength, Charset cs) {
        super(writer);
        if (writer == null) {
            throw new NullPointerException();
        }
        if (indentUnit <= 0) {
            throw new IllegalArgumentException("indentUnit must greater than 0");
        }
        if (lineLength > 0 && indentUnit >= lineLength) {
            throw new IllegalArgumentException("indentUnit must less than lineLength");
        }
        if (tabLength < 0) {
            throw new IllegalArgumentException("tabLength must greater equal 0");
        }
        if (cs == null) {
            cs = Charset.forName(new InputStreamReader(System.in).getEncoding());
        }
        this.indentUnit = indentUnit;
        this.lineLength = lineLength;
        this.tabLength = tabLength;
        this.cs = cs;
    }

    /**
     * インデントを増やす
     * @throws IOException 
     */
    public void indent() throws IOException {
        indentLevel++;
        indentCount = indentUnit * indentLevel;
        char[] c = new char[indentCount];
        Arrays.fill(c, ' ');
        indentString = String.valueOf(c);
        // 文字を出力した後でインデントが出力位置を超えた場合、インデント位置まで空白を出力する
        if (currentPos > 0) {
            while (indentCount > currentPos) {
                out.write(' ');
                currentPos++;
            }
        }
    }

    /**
     * インデントを減らす
     */
    public void unIndent() {
        indentLevel--;
        if (indentLevel < 0) {
            indentLevel = 0;
        }
        indentCount = indentUnit * indentLevel;
        if (indentLevel > 0) {
            char[] c = new char[indentCount];
            Arrays.fill(c, ' ');
            indentString = String.valueOf(c);
        } else {
            indentString = "";
        }
    }

    /**
     * インデントレベルを返す
     * @return
     */
    public int getIndentLevel() {
        return indentLevel;
    }

    /**
     * 1行あたりの最大文字数を返す
     * @return
     */
    public int getLineLength() {
        return lineLength;
    }

    /**
     * 1行あたりの最大文字数を設定する。<br/>
     * 0以下であれば制限なし
     * @param value
     */
    public void setLineLength(int value) {
        lineLength = value;
    }

    /**
     * タブの文字数を返す
     * @return
     */
    public int getTabLength() {
        return tabLength;
    }

    /**
     * インデントごとの空白の数を返す
     * @return
     */
    public int getIndentUnit() {
        return indentUnit;
    }

    /**
     * 行内位置を設定する。
     * 通常ファイルやコンソールに出力するだけであれば外部から出力位置を変更する必要はないが、コンソールからの入力を必要とする場合はユーザーの入力に合わせて調整する必要がある。
     * @return
     */
    public void setCurrentPos(int currentPos) {
        this.currentPos = currentPos;
    }

    /**
     * 現在の行内位置を返す
     * @param currentPos
     */
    public int getCurrentPos() {
        return currentPos;
    }

    /**
     * 1文字ごとに最大文字数を超えるかどうかのチェックを行い、超えるようであれば改行を挿入する。<br/>
     * 行頭には指定されたインデントを挿入する。<br/>
     * インデント・最大文字数の設定に関わらず、インデントだけで改行されることは有りえない。
     */
    public void write(char[] cbuf, int off, int len) throws IOException {
        for (int i = 0; i < len; i++) {
            char c = cbuf[off + i];
            // 基本的に制御文字は無視する。
            if (Character.isISOControl(c)) {
                if (c != '\r' && c != '\n' && c != '\t') {
                    continue;
                }
            }
            // 行頭であればインデントを出力する
            if (currentPos == 0 && c != '\r' && c != '\n') {
                out.write(indentString);
                currentPos = indentCount;
            }
            // TABの場合、指定された文字数または行末まで空白を出力する
            if (c == '\t') {
                for (int j = 0; j < tabLength && (lineLength <= 0 || currentPos < lineLength); j++) {
                    out.write(' ');
                    currentPos++;
                }
                continue;
            }
            // 改行の場合、文字数をリセットする
            if (c == '\r' || c == '\n') {
                out.write(c);
                if (LAST_LINE_SEPARATOR_CHAR == c) {
                    currentPos = 0;
                }
                continue;
            }
            // 出力する文字のバイト数を計算する
            // 本来はバイト数ではなく、表示に必要な文字幅で計算するべきだがそんなのは取得しようもないので仕方ない・・・
            int byteCount = String.valueOf(c).getBytes(cs.name()).length;
            // 文字数制限がある場合に文字数のチェックを行う
            if (lineLength > 0) {
                // サロゲート文字は間に改行を入れないようにする
                if (!Character.isLowSurrogate(c)) {
                    // インデント以外の文字を出力しており、最大文字数を超えるようであれば改行する
                    if (currentPos > indentCount && currentPos + byteCount > lineLength) {
                        out.write(LINE_SEPARATOR);
                        // 行頭にインデントを挿入する
                        out.write(indentString);
                        currentPos = indentCount;
                    }
                }
            }
            // 文字を出力する
            out.write(c);
            // サロゲート文字は表示上はchar2つで1文字なので下位サロゲート文字のカウントはしない
            if (!Character.isLowSurrogate(c)) {
                currentPos += byteCount;
            }
        }
    }

    /**
     * インデントレベルをゼロにする
     */
    public void clearIndent() {
        indentLevel = 0;
        indentCount = 0;
        indentString = "";
    }

    public void writeLine(String text) throws IOException {
        write(text);
        write(LINE_SEPARATOR);
        
    }

}