torutkのブログ

ソフトウェア・エンジニアのブログ

Windows 7でJava/SwingのJFileChooserがシンボリックリンクをたどれない

Windows OSのファイルシステムNTFS)には、ショートカットのほかに、ジャンクションおよびシンボリックリンクの仕組みがあります。

あるディレクトリ(たとえば、C:\Program Files\Java)に対して、C:\Java というジャンクションまたはシンボリックリンクを作ることができます。Windows Vista/7では、mklinkというコマンドが標準搭載されています。Windows XPの場合、Microsoftからjunction.exeを入手するとjunctionを作成できます。

C:\>mklink /J C:\java "C:\Program Files\Java"
C:\java <<===>> C:\Program Files\Java のジャンクションが作成されました

C:\>dir
2011/03/27  16:54    <JUNCTION>     java [C:\Program Files\Java]
  :
C:\>

Javaには、標準搭載のGUIライブラリにファイル選択ダイアログがあります。

  1. java.awt.FileDialog
  2. javax.swing.JFileChooser

両者とも、ショートカットには対応しています。
Windows 7において、ジャンクションについては、前者は問題なくその中に入っていけますが、後者は、ジャンクションのディレクトリをダブルクリックしても中に入っていけません。(Java SE 6 Update 23で確認)

  • Java SE 7 Update 3では問題なし(2012/02/19追記)

Windows XPで、別途Microsoftからjunction.exeを入手してジャンクションを作成した場合は後者でも問題なくジャンクションの先のディレクトリ以下に入ることができていました。

awtは、Windows OSのAPI(Win32API)が提供するファイル選択コントロールを内部で使用していると思われますが、swingは自前でファイル選択ダイアログ機能を実装しています。そこで、swingの実装を追ってみることにします。

ソースファイルの追跡メモ

JFileChooser.javaを追う

まず、JDKAPIソースコードjdkに付属)を展開し、javax/swing/JFileChooser.java をざざっと見てみます。Swingの各部品は、見栄えに関する処理を XXUIといった名前のクラスに委譲します。JFileChooserのソースからそれらしいものを探すと、javax.swing.plaf.FileChooserUIというクラス名が見つかります。が、これはabstractクラスで、実際はLook & Feel毎に実装クラスが存在します。実装クラスを調べるため、JFileChooserをnewして、getUI()を呼び、戻り値に対してgetClass()を呼び、実装クラス名を確認します。この手の調査は、Groovyを使うと簡単です。
で、得られた名前は com.sun.java.swing.plaf.windows.WindowsFileChooserUI でした。
このクラスは、JDKの付属ソースには含まれていないので、JDKのソースから探します。

WindowsFileChooserUI.javaを追う

com.sun.java.swing.plaf.windows.WindowsFileChooserUI.javaを見ると、WindowsFileChooserUIは、javax.swing.plaf.basic.BasicFileChooserUIクラスを継承しています。先にBasicFileChooserUIを見ることにします。

BasicFileChooserUI.javaを追う

private void changeDirectory(File dir) というメソッドが見つかりました。

    private void changeDirectory(File dir) {
	JFileChooser fc = getFileChooser();
	// Traverse shortcuts on Windows
        if (dir != null && FilePane.usesShellFolder(fc)) {
	    try {
                ShellFolder shellFolder = ShellFolder.getShellFolder(dir);
                if (shellFolder.isLink()) {
                    File linkedTo = shellFolder.getLinkLocation();
                    if (linkedTo != null && fc.isTraversable(linkedTo)) {
                        dir = linkedTo;
                    } else {
                        return;
                    }
                }
	    } catch (FileNotFoundException ex) {
		return;
	    }
	}
	fc.setCurrentDirectory(dir);
        if (fc.getFileSelectionMode() == JFileChooser.FILES_AND_DIRECTORIES &&
            fc.getFileSystemView().isFileSystem(dir)) {

            setFileName(dir.getAbsolutePath());
        }
    }

このメソッドで、ディレクトリ移動が行われない場合の条件は以下となります。

  • 引数dirがnullでない
  • sun.swing.FilePane.usesShellFolder(fc)がtrue
  • sun.awt.shell.ShellFolder.getShellFolder(dir)で取得したshellFolderについて、shellFolder.isLink()がtrue
  • shellFolder.getLinkLocation()がnullか、またはfc.isTraversable(linkedTo)がfalse

そこで、ジャンクションのディレクトリに対してこれら条件を評価してみました。

Windows 7においてGroovyConsoleで以下を実行してみました。

dir = new File("C:\\java")
fc = new javax.swing.JFileChooser()
println sun.swing.FilePane.usesShellFolder(fc)
folder = sun.awt.shell.ShellFolder.getShellFolder(dir)
println folder.isLink()
println folder.getLinkLocation()

結果は

true
true
null

となりました。


ここまでのところ、ジャンクションまたはシンボリックリンクへのディレクトリ移動ができないのは、ShellFolderのgetLinkLocation()がnullを返すことが原因のようです。
ShellFolderのgetShellFolder(...)が返すインスタンスは、sun.awt.shell.Win32ShellFolder2クラスでした。

Windows XPでは、2番目のisLink()の結果がfalseになります。なので、junctionとして作ったファイルについて、ShellFolderのisLink()がWindows 7でtrueを返すようになったのが挙動の違いとなります。
ただし、ジャンクションやシンボリックリンクであるファイルに対してisLink()がtrueを返すのは妥当で、Windows XPにおいてfalseを返していたのは妥当ではないと思います。

isLink()がtrueを返す場合、次のgetLinkLocation()がnullを返されると困ってしまいます。

Win32ShellFolder2.javaを追う

Win32ShellFolder2のgetShellFolderメソッドのコードを見ると、nativeメソッドgetShellFolder(...)を呼んでいます。JNIを追っていくと、JDKソースコードj2se/src/windows/native/sun/windows/ShellFolder2.cppにその実装があります。
なんとなく肝っぽい部分を抜き出すと、(エラー処理や戻り値を削除)

  IShellLinkW* psl;
  ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (LPVOID *)&psl);
  psl->QueryInterface(IID_IPersistFile, (void**)&ppf);
  ppf->Load(wstr, STGM_READ);
  psl->Resolve(NULL, 0);
  psl->GetIDList(&pidl);

といった部分で、問題が起きているようです。

Visual Studio 2010 (Express)で、このコードを記述し、ファイルパスとしてショートカットを指定するといずれの呼び出しも成功しますが、ジャンクションを指定すると、ppf->Load(...)でエラーが返ります。

int _tmain(int argc, _TCHAR* argv[])
{
    if (argc <= 1) {
        printf("引数にディレクトリへのパスを指定してください\n");
        return FALSE;
    }

    CoInitialize(NULL);
    IShellLink* psl;
    CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, (LPVOID*)&psl);
    IPersistFile* ppf;
    HRESULT hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
    if (hres != S_OK) {
        printf("ERROR: QueryInterface IID_IPersistFile\n");
        return FALSE;
    }
  
    hres = ppf->Load(argv[1], STGM_READ);
    if (hres != S_OK) {
        printf("ERROR: IPersistFile::Load\n");
        return FALSE;
    }
    hres = psl->Resolve(NULL, 0);
    if (hres != S_OK) {
        printf("ERROR: IShellLink::Resolve\n");
        return FALSE;
    }
    LPITEMIDLIST pidl = NULL;
    hres = psl->GetIDList(&pidl);
    if (hres != S_OK) {
        printf("ERROR: IShellLink::GetIDList\n");
        return FALSE;
    }
    printf("GetIDList => %ld\n", pidl);

    printf("No error. It may be success.\n");

    ppf->Release();
    psl->Release();
	return 0;
}

回避手段

ショートカットが辿れなくてもよい場合、JFileChooserに対して以下の設定をすると、ジャンクションを辿れるようになります。

  JFileChooser chooser = new JFileChooser();
  chooser.putClientProperty("FileChooser.useShellFolder", false);