JavaFXアプリケーションを特定のファイルタイプに関連付ける [Windows MSI編]

パッケージ製品開発担当の大です。こんにちは。

前回に引き続き、Javaパッケージャツールのファイルの関連付け機能を試してみます。今回はWindowsでMSI形式のインストーラを作成してみます。

準備

Mac編で使用したものと同じファイル(tekito.panda)を関連付けテスト用に使用します。現時点では何も関連づけられていないので、ファイルのプロパティの「プログラム」は「アプリの選択」、アイコンも白紙になっています。

関連付け前

関連付け前(クリックで拡大)

中身も前回同様にただのUTF-8のテキストです。

PS C:¥Temp> cat .¥tekito.panda
ファイルの中身です。

アイコンは、検索して見つけたパブリックドメインなものを使わせていただきます。

app.icoとdoc.ico

app.icoとdoc.ico(クリックで拡大)

最新版のNetBeans(現時点で8.0.2)、Java8(現時点でJava8 Update 51)をインストールします。

さらに、WindowsでMSI形式のインストーラを作成する場合は、WiX Toolsetが必要です。

インストール後、WiXのcandle.exeの置いてあるフォルダを環境変数PATHに追加したら、準備完了です。

JavaFXプロジェクトの作成

NetBeans上で「ファイル>新規プロジェクト>JavaFX>JavaFXアプリケーション」を選択し、適当な名前でプロジェクトとアプリケーションクラスを作成します。

Mac編と同じく、プロジェクト名を「AssocTest」、アプリケーションクラスを「test.AssocTest」としました。

アプリケーションクラスもMac編と同じです。

AssocTest.java

package test;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class AssocTest extends Application {

     @Override
    public void start(Stage primaryStage) {
        Text text = new Text();
        StackPane root = new StackPane();
        root.getChildren().add(text);

        Scene scene = new Scene(root, 300, 100);

        primaryStage.setTitle("関連付けのテスト");
        primaryStage.setScene(scene);
        primaryStage.show();

        text.setText("Hello, World!");
    }

    public static void main(String[] args) {
        launch(args);
    }
}

実行すると、

実行結果

実行結果(クリックで拡大)

こんな感じで表示されました。

ネイティブインストーラを作成

では、前回と同様にbuild.xmlに直接記述します。

Mac編と違うのは、アイコンのファイル名だけです。

  <target name="-post-jfx-deploy">
    <fx:deploy width="${javafx.run.width}" height="${javafx.run.height}" nativeBundles="all"
               outdir="${basedir}/${dist.dir}" outfile="${ant.project.name}">
      <fx:preferences install="true" />
      <fx:info title="${application.title}" vendor="${application.vendor}" description="${application.desc}">
        <fx:icon href="${basedir}/res/app.ico"/>
        <fx:association extension="panda PANDA" description="関連付けテスト用" icon="${basedir}/res/doc.ico" />
      </fx:info>
      <fx:permissions elevated="true"/>
      <fx:application name="${ant.project.name}" mainClass="${javafx.main.class}" />
      <fx:resources>
        <fx:fileset dir="${basedir}/${dist.dir}" includes="${ant.project.name}.jar"/>
      </fx:resources>
    </fx:deploy>
  </target>

この設定でビルドしてみます。

(前略)
Launching task from C:¥Program Files¥Java¥jdk1.8.0_51¥jre¥..¥lib¥ant-javafx.jar
ベースJDKがありません。パッケージはシステムJREを使用します。
構成の問題のため、バンドラMSI Installerがスキップされました: java.lang.NullPointerException

うーん、NullPointerExceptionが発生していますね。

mimetype属性が無いとエラー

NetBeansのツール>オプション>Java>ant>詳細レベルを「デバッグ」に変更して、再度ビルドしてみます。

Launching task from C:¥Program Files¥Java¥jdk1.8.0_51¥jre¥..¥lib¥ant-javafx.jar
(前略)
ベースJDKがありません。パッケージはシステムJREを使用します。
Running [C:¥Program Files¥Java¥jdk1.8.0_51¥jre¥bin¥java, -version]
Running [C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥candle.exe, /?]
Detected [C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥candle.exe] version [3.9]
Running [C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥light.exe, /?]
Detected [C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥light.exe] version [3.9]
WiX 3.6 detected. Enabling advanced cleanup action.
com.oracle.tools.packager.ConfigException: java.lang.NullPointerException
at com.oracle.tools.packager.windows.WinMsiBundler.validate(WinMsiBundler.java:342)
(中略)
at org.netbeans.core.execution.RunClassThread.run(RunClassThread.java:153)
Caused by: java.lang.NullPointerException
at com.oracle.tools.packager.windows.WinMsiBundler.validate(WinMsiBundler.java:329)
... 71 more
構成の問題のため、バンドラMSI Installerがスキップされました: java.lang.NullPointerException

NullPointerExceptionは、com.oracle.tools.packager.windows.WinMsiBuilder.validate で発生しているようです。

せっかく行番号も出てるので、ソースを見てみます。OpenJFXのサイトからソースを取得してきます。

$ hg clone http://hg.openjdk.java.net/openjfx/8u-dev/rt

「hg tags」でタグを見てみると、今使用しているバージョン(8u51)の後、8u60の作業がだいぶ進んでいるようなので、8u51の最後っぽいソースに更新します。

$ hg update 8u51-b16

Javaパッケージャツールのソースは、rt/modules/fxpackeger にあります。WinMsiBuilder.javaを開いて、NullPointerExceptionが出ているという329行目を見てみます。

List<Map<String, ? super Object>> associations = FILE_ASSOCIATIONS.fetchFrom(p);
if (associations != null) {
    for (int i = 0; i < associations.size(); i++) {
        Map<String, ? super Object> assoc = associations.get(i);
        List<String> mimes = FA_CONTENT_TYPE.fetchFrom(assoc);
        if (mimes.size() > 1) {
            throw new ConfigException(
                    MessageFormat.format(I18N.getString("error.too-many-content-types-for-file-association"), i),
                    I18N.getString("error.too-many-content-types-for-file-association.advice"));
        }
    }
}

associationの定義の中から取り出したmimesがnullになっているみたいですね。

<fx:association>の説明では、mimetype属性はLinuxではrequired、Macではmimetypeかextensionどちらかが必要となっているので、ほんとはWindowsでは必要ないと思いますが、エラー回避のためmimetype属性を追加して適当な値を設定し、再度実行してみます。

<fx:association extension="panda PANDA" mimetype="application/panda" description="関連付けテスト用" icon="${basedir}/res/doc.ico"/>

(前略)
Running [C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥candle.exe, -nologo, C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥windows¥AssocTest.wxs, -ext, WixUtilExtension, -out, C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥tmp¥AssocTest.wixobj] in C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥images¥win-msi.image¥AssocTest
AssocTest.wxs
C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥windows¥AssocTest.wxs(41) : error CNDL0104 : Not a valid source file; detail: 指定されたエンコードに無効な文字があります。 行 25、位置 48。
Exec failed with code 104 command [[C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥candle.exe, -nologo, C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥windows¥AssocTest.wxs, -ext, WixUtilExtension, -out, C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥tmp¥AssocTest.wixobj] in C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler9203608614261371977¥images¥win-msi.image¥AssocTest

別のエラーメッセージが出ました。ひとまずmimesの件はクリアできたようです。

descriptionに日本語が入っているとエラー

新しく表示されたエラーメッセージを見ると、どうやら文字コードの問題のようです。

descriptionに指定している「関連付けテスト用」という文字列が怪しいので、適当に変えてみます。

<fx:association extension="panda PANDA" mimetype="application/panda" description="file association test" icon="${basedir}/res/doc.ico"/>

Running [C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥light.exe, -nologo, -spdb, -sice:60, C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler8983641780838595123¥tmp¥AssocTest.wixobj, -ext, WixUtilExtension, -out, C:¥Users¥hos¥Documents¥NetBeansProjects¥AssocTest¥dist¥bundles¥AssocTest-1.0.msi] in C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler8983641780838595123¥images¥win-msi.image¥AssocTest
light.exe : error LGHT0001 : 項目は既に追加されています。辞書のキー: 'reg9FB673D9F29DBB43665C5EA1A3BD540F' 追加されるキー:'reg9FB673D9F29DBB43665C5EA1A3BD540F'

Exception Type: System.ArgumentException

Stack Trace:
場所 System.Collections.Hashtable.Insert(Object key, Object nvalue, Boolean add)
場所 System.Collections.Hashtable.Add(Object key, Object value)
場所 Microsoft.Tools.WindowsInstallerXml.Binder.SetComponentGuids(Output output)
場所 Microsoft.Tools.WindowsInstallerXml.Binder.BindDatabase(Output output, String databaseFile)
場所 Microsoft.Tools.WindowsInstallerXml.Binder.Bind(Output output, String file)
場所 Microsoft.Tools.WindowsInstallerXml.Tools.Light.Run(String[] args)
Exec failed with code 1 command [[C:¥Program Files (x86)¥WiX Toolset v3.9¥bin¥light.exe, -nologo, -spdb, -sice:60, C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler8983641780838595123¥tmp¥AssocTest.wixobj, -ext, WixUtilExtension, -out, C:¥Users¥hos¥Documents¥NetBeansProjects¥AssocTest¥dist¥bundles¥AssocTest-1.0.msi] in C:¥Users¥hos¥AppData¥Local¥Temp¥fxbundler8983641780838595123¥images¥win-msi.image¥AssocTest

またエラーメッセージが変わりました。文字コードの件はクリアできたようです。

拡張子が重複しているとエラー

表示されているスタックトレースがJavaのものではないので、どうやら今回のエラーはWiX側で発生しているようです。

キーが重複しているということなので、extensionに指定している拡張子を小文字のものだけにしてみます。

<fx:association extension="panda" mimetype="application/panda" description="file association test" icon="${basedir}/res/doc.ico"/>
作成されたmsi

作成されたmsi(クリックで拡大)

今度はうまくいきました!

出来上がったmsiをダブルクリックしてインストールしてみると、tekito.pandaに指定したアイコンが適用され、表示が変わりました。

関連付け後

関連付け後(クリックで拡大)

tekito.pandaをダブルクリックすると、無事にAssocTestが起動しました。

ダブルクリックされたファイルの情報をアプリに渡す

さて、今度はダブルクリックされたtekito.pandaの情報をアプリに渡すようにしてみます。

MacではgetParameters()でファイル名が取得できなくて苦労しましたが、Windowsでは難なく取得できたので、最初のコードを以下のように変更することで、簡単にファイルの中身を表示することができました。

AssocTest.java

package test;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class AssocTest extends Application {

     @Override
    public void start(Stage primaryStage) {
        Text text = new Text();
        StackPane root = new StackPane();
        root.getChildren().add(text);

        Scene scene = new Scene(root, 300, 100);

        primaryStage.setTitle("関連付けのテスト");
        primaryStage.setScene(scene);
        primaryStage.show();

        List<String> files = getParameters().getUnnamed();
        if (files != null && !files.isEmpty()) {
            byte[] bytes = Files.readAllBytes(Paths.get(files.get(0)));
            text.setText(new String(bytes, "utf-8"));
        } else {
            text.setText("ファイルが指定されていません。");
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
実行結果

実行結果(クリックで拡大)