CyclicFileWriter

Log4jの中に自動的に出力先のファイルをバックアップするクラスがあるが、ログだけではなく、自分でファイルに出力する時にもその機能が使えないかと思って、作ってみた。
実際にバックアップする機能は別のクラスになっていて、このクラスはバックアップのトリガーを決めるだけになっている。

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;

/**
 * 一つのファイルを繰り返し使用するライタ。<br/>
 * writerを再作成するタイミングの決定や、その際に必要な処理は{@link CyclicPolicy}の実装クラスに記述する。<br/>
 */
public class CyclicFileWriter extends Writer {

    public static class FileContext {
        private File file;
        private long length;
        private long creationTime;
        public FileContext(File file, long length, long creationTime) {
            this.file = file;
            this.length = length;
            this.creationTime = creationTime;
        }
        public File getFile() {
            return file;
        }
        /**
         * ファイルに出力したバイト数を返す。<br/>
         * @return
         */
        public long getLength() {
            return length;
        }
        /**
         * ファイルを作成した日時をミリ秒で返す。<br/>
         * {@link CyclicFileWriter}のインスタンス作成時にファイルが存在していた場合はその時点での最終更新日時となる。
         * @return
         */
        public long getCreationTime() {
            return creationTime;
        }
    }

    public interface CyclicPolicy {
        /**
         * ファイルをクリアする必要があるか否かを返す。<br/>
         * @param context
         * @return
         */
        boolean checkCyclic(FileContext context);
        /**
         * ファイルをクリアする際に呼ばれるメソッド。<br/>
         * 必要に応じてファイルのバックアップなどを行う。<br/>
         * それまで使用していたライタのflush・closeは済んでいる。<br/>
         * @param context
         * @throws IOException
         */
        void cyclic(FileContext context) throws IOException;
    }

    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
    private static final char LAST_LINE_SEPARATOR_CHAR = LINE_SEPARATOR.charAt(LINE_SEPARATOR.length() - 1);

    // 実際に使用するライタ
    private OutputStreamWriter writer;

    // 改行のたびにCyclicPolicy#checkCyclic()を呼ぶかどうかのフラグ
    private boolean checkAtNewLine = true;

    // 書き込み対象のファイル
    private File file;

    // ファイルに出力したバイト数
    private long[] length = new long[1];

    // ファイルの作成日時
    private long creationTime;

    // チェックしたかどうかのフラグ
    private boolean checked;

    // 再使用時の処理内容実装オブジェクト
    private CyclicPolicy cyclicPolicy;

    /**
     * システムのデフォルトエンコードでライタを作成する。<br/>
     * 指定されたファイルがすでにあれば追記される。<br/>
     * @param cyclicPolicy
     * @param file
     * @throws IOException
     */
    public CyclicFileWriter(CyclicPolicy cyclicPolicy, File file) throws IOException {
        this(cyclicPolicy, file, true, null);
    }

    /**
     * エンコードとチェックのタイミングを指定してライタを作成する。<br/>
     * 指定されたファイルがすでにあれば追記される。<br/>
     * @param cyclicPolicy
     * @param file
     * @param checkAtNewLine
     * @param encoding
     * @throws IOException
     */
    public CyclicFileWriter(CyclicPolicy cyclicPolicy, File file, boolean checkAtNewLine, String encoding) throws IOException {
        if (cyclicPolicy == null) {
            throw new NullPointerException();
        }
        if (file == null) {
            throw new NullPointerException();
        }
        // ファイルがすでにある場合は、その更新日時とファイルサイズを取得する。
        // 厳密には作成日時≠更新日時だが仕方ない
        if (file.exists()) {
            creationTime = file.lastModified() + SystemUtils.getOffsetCurrentTimeMillis();
            length[0] = file.length();
        } else {
            creationTime = SystemUtils.currentTimeMillis();
        }
        this.cyclicPolicy = cyclicPolicy;
        if (encoding == null) {
            encoding = new OutputStreamWriter(System.out).getEncoding();
        }
        writer = new OutputStreamWriter(new CountOutputStream(new FileOutputStream(file, true), length), encoding);
        this.file = file;
        this.checkAtNewLine = checkAtNewLine;
    }

    /**
     * システムのデフォルトエンコードでライタを作成する。<br/>
     * 指定されたファイルがすでにあれば追記される。<br/>
     * @param cyclicPolicy
     * @param name
     * @throws IOException
     */
    public CyclicFileWriter(CyclicPolicy cyclicPolicy, String name) throws IOException {
        this(cyclicPolicy, new File(name));
    }

    /**
     * エンコードとチェックのタイミングを指定してライタを作成する。<br/>
     * 指定されたファイルがすでにあれば追記される。<br/>
     * @param cyclicPolicy
     * @param name
     * @param checkAtNewLine
     * @param encoding
     * @throws IOException
     */
    public CyclicFileWriter(CyclicPolicy cyclicPolicy, String name, boolean checkAtNewLine, String encoding) throws IOException {
        this(cyclicPolicy, new File(name), checkAtNewLine, encoding);
    }

    @Override
    public void close() throws IOException {
        writer.close();
    }

    @Override
    public void flush() throws IOException {
        writer.flush();
    }

    /**
     * 書き込む前に必要に応じてファイルをクリアする。
     */
    @Override
    public void write(char[] cbuf, int off, int len) throws IOException {
        synchronized (this) {
            if (checkAtNewLine) {
                // 改行ごとにチェックする場合はバッファの中の改行コードごとにチェックおよび出力
                while (off < cbuf.length && len > 0) {
                    if (!checked) {
                        if (cyclicPolicy.checkCyclic(createContext())) {
                            cyclic();
                        }
                        checked = true;
                    }
                    int pos = -1;
                    for (int i = 0; i < len; i++) {
                        if (cbuf[off + i] == LAST_LINE_SEPARATOR_CHAR) {
                            pos = i + 1;
                            break;
                        }
                    }
                    // 改行コードが無ければそのまま出力
                    if (pos == -1) {
                        writer.write(cbuf, off, len);
                        writer.flush();
                        return;
                    }
                    // 改行コードまで出力
                    writer.write(cbuf, off, pos);
                    writer.flush();
                    off += pos;
                    len -= pos;
                    checked = false;
                }
            } else {
                // 改行ごとにチェックしない場合は一回だけチェック
                if (cyclicPolicy.checkCyclic(createContext())) {
                    cyclic();
                }
                writer.write(cbuf, off, len);
                writer.flush();
            }
        }
    }

    /**
     * ライタを閉じて新規に作成しなおす。<br/>
     * @throws IOException
     */
    private void cyclic() throws IOException {
        String encoding = writer.getEncoding();
        writer.flush();
        writer.close();
        writer = null;
        cyclicPolicy.cyclic(createContext());
        writer = new OutputStreamWriter(new CountOutputStream(new FileOutputStream(file, false)), encoding);
        creationTime = SystemUtils.currentTimeMillis();
        length[0] = 0;
    }

    protected FileContext createContext() {
        return new FileContext(file, length[0], creationTime);
    }

    public File getFile() {
        return file;
    }

    public String getEncoding() {
        return writer.getEncoding();
    }

    public long getLength() {
        return length[0];
    }

    public long getCreationTime() {
        return creationTime;
    }

    public CyclicPolicy getCyclicPolicy() {
        return cyclicPolicy;
    }

}