Windows環境のJavaでショートカットを作ってみた（もしくはJNAでCOMバインディングしてみた）

## これまでのあらすじ

いまさらJavaでGUIやっているのだけれど、アプリ上からショートカットを作れっていわれた。ぐぐるさんに聞いてもあまり参考になる情報がない。まじかめんどいぞ。

:::note
ご注意
コードは JNA Version 5.10.0 を利用したものとなっています。
以下のテキストは9割がた与太なので、適当に読み流してください。
:::

## Javaじゃなかったらどうやって作るのん？

どうやらプログラム上からショートカットを作るには Windows Script Host なるものを使うらしい。

```vb:shortcut.vbs
' シェルオブジェクトを取得して
Set shell = WScript.CreateObject("WScript.Shell")
' ファイルの場所指定してショートカットオブジェクト生成して
Set shortCut = shell.CreateShortcut(shell.SpecialFolders("Desktop") + "\cmd.lnk")
' リンク先（今回はコマンドプロンプト）を設定して
Set systemEnv = shell.Environment("SYSTEM")
shortCut.TargetPath = systemEnv("COMSPEC")
' 保存
shortCut.Save()
```

なるほどこれをJNAで再現してやればいいのだな。

## JNAの予習

JNAのドキュメントにある、[Platform Library -> COM support](https://github.com/java-native-access/jna/blob/master/www/PlatformLibrary.md#com-support) あたりで実現できそうです。
ドキュメントによると方法は２つ。
`COMBindingBaseObject` を継承したクラスをつくって `oleMethod` をコールする方法がひとつめ。
もうひとつは`com.sun.jna.platform.win32.COM.util`パッケージのやつだぜってだけ書いてある。
こんな投げやりなドキュメントなんですが、[jna/contrib/msoffice/](https://github.com/java-native-access/jna/tree/master/contrib/msoffice) の方にMSOfficeを例にしたサンプルコードがあるらしいので、そっちを見ながら進めることにします。
コードジェネレーターなんてものもあるみたいです。そのうち試してみたいけれど、今回は除外。

## ひとつめのアプローチ

デモのコードがドキュメントに書いてあったのと違うじゃん。
`COMBindingBaseObject` を継承したクラスで `oleMethod` 呼ぶ方法でも実現はできるんですが、もう一段階手続きを簡略化してくれるクラスが準備されています。なのでこの `COMLateBindingObject` を使っていきます。
 `getProperty/setProperty` でプロパティの読み書きして `invoke` でメソッドを呼ぶ。
昔ながらのJavaっていう感じのアプローチだけれど、JNAっぽくはないなあ（個人的な見解）。

```java:WshShell.java
public class WshShell extends COMLateBindingObject {

	public WshShellTest() {
		super("WScript.Shell", false);
	}

	public String getSpecialFolder(String name) {
		VARIANT val = invoke("SpecialFolders", new VARIANT(name));
		return val.getValue().toString();
	}
	
	public WshShortcut createShortcut(String path) {
		VARIANT val = invoke("CreateShortcut", new VARIANT(path));		
		return new WshShortcut((IDispatch) val.getValue());
	}

	public class WshShortcut extends COMLateBindingObject {

		private WshShortcut(IDispatch iDispatch) {
			super(iDispatch);
		}

		public void setTargetPath(String path) {
			setProperty("TargetPath", path);
		}
		
		public void save() {
			invoke("Save");
		}
	}
	
	public static void main(String[] args) {
		// COMライブラリの初期化
		Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED);
		try {
			// シェルオブジェクトを取得して
			WshShell shell = new WshShell();
			// ファイルの場所指定してショートカットオブジェクト生成して
			WshShortcut shortcut = shell.createShortcut(shell.getSpecialFolder("Desktop") + "\\cmd.lnk");
			// リンク先（今回はコマンドプロンプト）を設定して
			shortcut.setTargetPath(System.getenv("COMSPEC"));
			// 保存
			shortcut.save();
		} finally {
			// COMライブラリの初期化解除
			Ole32.INSTANCE.CoUninitialize();
		}
	}
}
```

## ふたつめのアプローチ

デモのコードを読んでみたところ、どうやらインターフェイスにアノテーションを定義してマッピングをするらしい。
実際にはこんな感じです。こっちのほうがJNAっぽいなあ（個人的な見解）。
`@ComProperty`、`@ComMethod` のname はメソッド名がバインディング先と同名ならば指定しなくても大丈夫。大文字小文字は変えてもOK。get/setも忖度してくれます。 

```java:IWshShell.java
@ComObject(progId = "WScript.Shell")
public interface IWshShell {

	@ComProperty(name = "SpecialFolders")
	String getSpecialFolder(String name);

	@ComMethod
	IDispatch createShortcut(String path);

	public interface IWshShortcut {

		@ComProperty
		void setTargetPath(String path);

		@ComMethod
		void save();
	}

	public static void main(String[] args) {
		// COMライブラリの初期化
		Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED);
		try {
			// ファクトリを準備して
			ObjectFactory factory = new ObjectFactory();
			// シェルオブジェクトを取得して
			IWshShell shell = factory.createObject(IWshShell.class);
			// ファイルの場所指定してショートカットオブジェクト生成して
			String linkpath = shell.getSpecialFolder("Desktop") + "\\cmd.lnk";
			IDispatch dispatch = shell.createShortcut(linkpath);
			IWshShortcut shortcut = factory.createProxy(IWshShortcut.class, dispatch);
			// リンク先（今回はコマンドプロンプト）を設定して
			shortcut.setTargetPath(System.getenv("COMSPEC"));
			// 保存
			shortcut.save();
		}finally {
			// COMライブラリの初期化解除
			Ole32.INSTANCE.CoUninitialize();
		}
	}
}
```


まあ、なにはともあれ、どちらの方法でも、簡単にショートカットができました。すごい。

## 一応、完成版のコードなど

WshShortcutの他のプロパティへのアクセスも記述したやつを載せておきます。せっかく書いたので。
基本的にはインターフェイスとアノテーションの方が好みなんですが、Factory呼ぶあたりを隠したかったので、両方のやり方の合わせ技みたいな感じにしてみました。

<details><summary>WshShell.java</summary>

```java:WshShell.java
package yamane;

import java.io.IOException;

import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.Ole32;
import com.sun.jna.platform.win32.Variant.VARIANT;
import com.sun.jna.platform.win32.COM.COMLateBindingObject;
import com.sun.jna.platform.win32.COM.IDispatch;
import com.sun.jna.platform.win32.COM.util.ObjectFactory;
import com.sun.jna.platform.win32.COM.util.annotation.ComMethod;
import com.sun.jna.platform.win32.COM.util.annotation.ComProperty;

/**
 * WshShell オブジェクト
 */
public class WshShell extends COMLateBindingObject {

	public WshShell() {
		super("WScript.Shell", false);
	}

	/**
	 * SpecialFolders プロパティ(コレクション)から特定の要素を取得します。
	 * @param name 要素名
	 * @return フォルダーのパス
	 */
	public String getSpecialFolder(String name) {
		VARIANT val = invoke("SpecialFolders", new VARIANT(name));
		return val.getValue().toString();
	}

	/**
	 * ショートカットオブジェクトを生成します。
	 * @param path 生成するショートカットのパス
	 */
	public IWshShortcut createShortcut(String path) {
		VARIANT val = invoke("CreateShortcut", new VARIANT(path));		
		return new ObjectFactory().createProxy(IWshShortcut.class, (IDispatch) val.getValue());
	}

	/**
	 * WshShortcut インターフェイス
	 */
	public interface IWshShortcut {

		/**
		 * ショートカットの実行可能ファイルへのパスを設定します。
		 * @param path ショートカットの実行可能ファイルへのパス
		 */
		@ComProperty
		void setTargetPath(String path);

		/**
		 * ショートカット実行時の引数を設定します。
		 * @param args ショートカット実行時の引数
		 */
		@ComProperty
		void setArguments(String args);
		
		/**
		 * ショートカットの作業ディレクトリを設定します。
		 * @param dir 作業ディレクトリのパス
		 */
		@ComProperty
		void setWorkingDirectory(String dir);
		
		/**
		 * アイコンをショートカットに割り当てます。
		 * 絶対パスと、アイコンに関連付けられているインデックスを含める必要があります。
		 * @param path アイコンの格納場所を示すパス+インデックス
		 */
		@ComProperty
		void setIconLocation(String path);
		
		/**
		 * ショートカットに割り当てるキーの組み合わせを設定します。
		 * @param keys キーの組み合わせを表す文字列
		 */
		@ComProperty
		void setHotkey(String keys);
		
		/**
		 * ウィンドウ スタイルをショートカットに割り当てます。
		 * 1. ウィンドウをアクティブにして表示
		 * 3. ウィンドウを最大化して表示
		 * 7. ウィンドウを最小化して表示
		 * @param style キーの組み合わせを表す文字列
		 */
		@ComProperty
		void setWindowStyle(Integer style);
		
		/**
		 * ショートカットの説明文（コメント）を設定します。
		 * @param text 説明文（コメント）
		 */
		@ComProperty
		void setDescription(String text);
		
		/**
		 * ショートカットオブジェクトを保存します。
		 */
		@ComMethod
		void save();
	}

	/**
	 * 使い方
	 * @param args 参照してない
	 */
	public static void main(String[] args) throws IOException {
		// COMライブラリの初期化
		Ole32.INSTANCE.CoInitializeEx(Pointer.NULL, Ole32.COINIT_MULTITHREADED);
		try {
			// シェルオブジェクトを取得
			WshShell shell = new WshShell();
			// ファイルの場所指定してショートカットオブジェクト生成
			IWshShortcut shortcut = shell.createShortcut(shell.getSpecialFolder("Desktop") + "\\cmd.lnk");
			// 引数
			shortcut.setArguments("/?");
			// 作業ディレクトリ
			shortcut.setWorkingDirectory(shell.getSpecialFolder("MyDocuments"));
			// リンク先（今回はコマンドプロンプト）を設定
			shortcut.setTargetPath(System.getenv("COMSPEC"));
			// 最大化
			shortcut.setWindowStyle(3);
			// ホットキー
			shortcut.setHotkey("CTRL+SHIFT+F");
			// コメント
			shortcut.setDescription("ショートカットサンプル");
			// アイコン
			shortcut.setIconLocation("C:\\Windows\\System32\\calc.exe, 0");
			// 保存
			shortcut.save();
		} finally {
			// COMライブラリの初期化解除
			Ole32.INSTANCE.CoUninitialize();
		}	
	}
}
```
</details>

## 参考ページ

https://github.com/java-native-access/jna/blob/master/www/PlatformLibrary.md#com-support

https://docs.microsoft.com/ja-jp/previous-versions/windows/scripting/cc364436(v=msdn.10)