メモの日々


2013年09月08日(日) [長年日記]

[java] java.util.PropertiesでWindowsのパス表記をエスケープせずに読み込みたい

問題点

Javaのプログラムで

path = C:\aaa\bbb bbb\あああ\ccc

とWindowsのパス表記で書いたファイルを次のコードのようにしてjava.util.Properties#load()を使って読み込むと、

C:aaabbb bbbあああccc

とバックスラッシュが削除されてしまう。

import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Properties;

public class PropertiesでのWindowsパス表記読込 {
    public static void main(String[] args) throws IOException {
        try (Reader r = Files.newBufferedReader(
                Paths.get("test.properties"), Charset.forName("UTF-8"))) {
            Properties p = new Properties();
            p.load(r);
            System.out.println(p.getProperty("path"));
        }
    }
}

こうなるのは、load()メソッドが

このメソッドは、無効なエスケープ文字の前のバックスラッシュ文字 \ をエラーとして処理しない。バックスラッシュは自動的に削除される。

という仕様だから。

この問題はパス区切りのバックスラッシュを2つ重ねて書くかバックスラッシュの代わりにスラッシュを使うようにすれば解決されるんだけど、この対処は面倒だ。そんなことせずにそのまま読み込みたい。

そもそもPropertiesクラスがバックスラッシュを特別扱いする機能って必要なのかな?なんでこんな仕様なんだっけ…

バックスラッシュを二重化するフィルタを挟んで対処してみる

バックスラッシュを特別扱いする機能は要らない(バックスラッシュは単にバックスラッシュとして解釈すればよい)ので、ファイル内の「\」を「\\」に変換するフィルタを作ってそれを挟んで読み込むようにしたらどうだろうか。

import java.io.FilterReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Properties;

public class PropertiesでのWindowsパス表記読込2 {
    public static void main(String[] args) throws IOException {
        try (Reader r = new Backslash二重化フィルタ(Files.newBufferedReader(
                Paths.get("test.properties"), Charset.forName("UTF-8")))) {
            Properties p = new Properties();
            p.load(r);
            System.out.println(p.getProperty("path"));
        }
    }
}

class Backslash二重化フィルタ extends FilterReader {
    private boolean inInBackslash = false;

    public Backslash二重化フィルタ(Reader r) { super(r); }

    @Override
    public int read() throws IOException {
        synchronized (lock) { return read1(); }
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        if (len <= 0) return 0;
        int result = 0;
        synchronized (lock) {
            for (int i = 0; i < len; ++i) {
                int c = read1();
                if (c < 0) break;
                cbuf[off + i] = (char)c;
                ++result;
            }
        }
        return result == 0 ? -1 : result;
    }

    @Override
    public long skip(long n) throws IOException {
        throw new IOException("not supported");
    }

    @Override
    public boolean ready() throws IOException { return inInBackslash; }

    @Override
    public boolean markSupported() { return false; }

    @Override
    public void mark(int readAheadLimit) throws IOException {
        throw new IOException("not supported");
    }

    @Override
    public void reset() throws IOException {
        throw new IOException("not supported");
    }

    private int read1() throws IOException {
        if (inInBackslash) {
            inInBackslash = false;
            return '\\';
        }
        int result = super.read();
        if (result == '\\') inInBackslash = true;
        return result;
    }
}

このプログラムを動かすと、

C:\aaa\bbb bbb\あああ\ccc

とファイルに書いた文字列がそのまま表示された。OKOK。

こういう対処は見かけないけれど、何か問題あるかな?