Magnolia Tech

いつもコードのことばかり考えている人のために。

java.net.URLConnectionのguessContentTypeFromNameが使うMIME Type設定は実行時に変更することはできない

java.net.URLConnectionのguessContentTypeFromNameは、content.types.user.tableというシステムプロパティで定義された内容で任意のMIME typeを推測できるようになります。

公式ドキュメントにも書かれています。

URLConnection (Java Platform SE 8)

では実際にそれを確かめてみようと、テストコードの中でSystem.setProperty("content.types.user.table", "/path/to/content-types.properties")と指定してもさっぱり有効になりませんでした。

例えば、拡張子.csstext/cssと判定させるためには、以下のような設定ファイルを用意します。

text/css: \
        description=Cascading Style Sheets;\
        file_extensions=.css

以下のようなコードを用意して、設定ファイルを読み込んでみましが、mimeTypeにはnullが入りました。

System.setProperty("content.types.user.table", "/path/to/content-types.properties")
val mimeType = URLConnection.guessContentTypeFromName("test.css")

実行時ではなく、予めsbt -Dcontent.types.user.table=/path/to/content-types.propertiesというふうに実行前に設定されるようにしておくと意図した通りにmimeTypeにはtext/cssが入ります。

java.net.URLConnectionのguessContentTypeFromNameのコードを追いかける

これは何が起きているのでしょうか?

実は最初、Stack OverflowにSystem.setPropertyでセットすれば反映されると書かれていたので、それを鵜呑みにして「正しく動かない!自分の書いたコードがおかしいのか?」と思いましたが、実際にはその回答が誤りでした。

順番に、java.net.URLConnectionguessContentTypeFromNameメソッドの挙動を、openJDK10のコードを例に追いかけていきましょう。

openJDKの該当するソースコードは以下の場所に有ります。

jdk10/master: be620a591379 src/java.base/share/classes/java/net/URLConnection.java

public static String guessContentTypeFromName(String fname) {
        return getFileNameMap().getContentTypeFor(fname);
}

public static FileNameMap getFileNameMap() {
    FileNameMap map = fileNameMap;

    if (map == null) {
        fileNameMap = map = new FileNameMap() {
            private FileNameMap internalMap =
                sun.net.www.MimeTable.loadTable();

            public String getContentTypeFor(String fileName) {
                return internalMap.getContentTypeFor(fileName);
            }
        };
    }

    return map;
}

MIME Typeに関連するデータは、FileNameMapというクラスに格納されていること、値が無ければ(nullならば)されていなければsun.net.www.MimeTable.loadTableを呼び出して、初期化していることが分かります。

なお、getContentTypeForFileNameMapのメソッドで、ほぼ単純なmap構造のデータから該当するMIME typeを取得する機能を提供します。

sun.net.www.MimeTable.loadTableの中身を追いかける

では、実際にMIME typeのテーブルを保持するsun.net.www.MimeTable.loadTableの中身を追いかけてみましょう。

sun.net.www.MimeTable.loadTableは以下のファイルに収録されています。

jdk10/master: be620a591379 src/java.base/share/classes/sun/net/www/MimeTable.java

public static FileNameMap loadTable() {
    MimeTable mt = getDefaultTable();
    return (FileNameMap)mt;
}

public static MimeTable getDefaultTable() {
    return DefaultInstanceHolder.defaultInstance;
}

private static class DefaultInstanceHolder {
    static final MimeTable defaultInstance = getDefaultInstance();

    static MimeTable getDefaultInstance() {
        return java.security.AccessController.doPrivileged(
            new java.security.PrivilegedAction<MimeTable>() {
            public MimeTable run() {
                MimeTable instance = new MimeTable();
                URLConnection.setFileNameMap(instance);
                return instance;
            }
        });
    }
}

loadTableから始まり、DefaultInstanceHolderクラスにdefaultInstanceというstaticなメンバが有ることが分かります。staticなクラスのstaticなメンバなので、クラス自体がロードされた時点で、getDefaultInstanceメソッドが実行され、defaultInstanceにはMIME typeのデータがロードされている、ということなのです。

更に、この時点でなぜかURLConnection.setFileNameMapを呼び出し、ロードしたMIME Typeをセットしています。getFileNameMapメソッドの中でやっているFileNameMap mapがnullか否かを判定するロジックっていらなくない?って思いますね。

実際にロードする部分のコードをもう少し追いかけてみましょう。

MimeTable() {
    load();
}

public synchronized void load() {
    Properties entries = new Properties();
    File file = null;
    InputStream in;

    // First try to load the user-specific table, if it exists
    String userTablePath = System.getProperty("content.types.user.table");
    if (userTablePath != null && (file = new File(userTablePath)).exists()) {
        try {
            in = new FileInputStream(file);
        } catch (FileNotFoundException e) {
            System.err.println("Warning: " + file.getPath()
                                + " mime table not found.");
            return;
        }
    } else {
        in = MimeTable.class.getResourceAsStream("content-types.properties");
        if (in == null)
            throw new InternalError("default mime table not found");
    }

    try (BufferedInputStream bin = new BufferedInputStream(in)) {
        entries.load(bin);
    } catch (IOException e) {
        System.err.println("Warning: " + e.getMessage());
    }
    parse(entries);
}

確かにcontent.types.user.tableというシステムプロパティが設定されていれば、そこからパスを取得するようになっていますね。存在しなければデフォルトのcontent-types.propertiesを取得しています。

ちなみに、このcontent.types.user.tableというシステムプロパティを元に任意のMIME Typeを設定する機能について、テストコードが存在しません。確かにこれではグローバルにシステムプロパティを汚染しないとテストが書けないですね…

おわりに

以上、java.net.URLConnectionguessContentTypeFromNameにおけるcontent.types.user.tableの取扱について、実際のJavaのライブラリのコードを追いかけてみました。

依存関係(sun.net.www.MimeTable.loadTablejava.net.URLConnectionに依存している)がおかしいとか、初回利用時ではなく、クラスロード時に強制的に初期化が行われ、以降は再設定もできないとか、そもそもテストが無いとか、よく考えるとguessContentTypeFromNameというメソッド自体java.net.URLConnectionではなく、独立したMIMEに関するクラスに所属しているべきでは?と、標準のJavaのライブラリでも色々と設計が気になるところが有るんだな、というのが今回の感想でした。