JavaでWebSocket使い方メモ

Java で WebSocket を実装してみる。
クライアント側は JavaScript で実装。

#環境
##AP サーバー
Tomcat 8.0.15

##ブラウザ
- IE11
- Google Chrome 38
- Firefox 33

#HelloWorld
##build.gradle
```groovy:build.gradle
apply plugin: 'war'

war.baseName = 'websocket'

repositories {
    mavenCentral()
}

dependencies {
    providedCompile 'javax.websocket:javax.websocket-api:1.1'
}
```

##フォルダ構成
```text:
|-build.gradle
`-src/main/
  |-java/sample/websocket/
  |  `-SampleWebSocket.java
  `-webapp/
     |-index.html
     `-script.js
```

##サーバー側実装
```java:SampleWebSocket.java
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class SampleWebSocket {
    
    @OnMessage
    public String echo(String message) {
        System.out.println(message);
        return message;
    }
}
```

##クライアント側実装
```html:index.html
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <span id="message"></span>
  </body>
</html>
```

```js:script.js
$(function() {
    var url = 'ws://localhost:8080/websocket/echo';
    var ws = new WebSocket(url);
    
    ws.onmessage = function(receive) {
        $('#message').text(receive.data);
    };
    
    ws.onopen = function() {
        ws.send('Hello WebSocket');
    }
});
```

##動作確認
war ファイルを作成して tomcat にデプロイし、ブラウザで `http://localhost:8080/websocket/` にアクセス。

![websocket.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/1e12b446-451a-6416-2cd2-201eef5191d0.jpeg)

```text:サーバーコンソール出力
Hello WebSocket
```

##説明
###サーバー側
```java:
@ServerEndpoint("/echo")
public class SampleWebSocket {
    
    @OnMessage
    public String echo(String message) {
        System.out.println(message);
        return message;
    }
}
```

- `@ServerEndpoiint` でアノテートしたクラスが、サーバー側の WebSocket のエンドポイントになる。
    - `value` 属性でパスを定義する。
- `@OnMessage` でメソッドをアノテートすると、クライアントからメッセージを受信したときに、そのメソッドが実行される。
    - 引数に受信したメッセージを受け取れる。
    - 戻り値が、そのままクライアントに返信される。

###クライアント側
```js:
$(function() {
    var url = 'ws://localhost:8080/websocket/echo';
    var ws = new WebSocket(url);
    
    ws.onmessage = function(receive) {
        $('#message').text(receive.data);
    };
    
    ws.onopen = function() {
        ws.send('Hello WebSocket');
    }
});
```

- `WebSocket` のインスタンスを生成するとサーバーへの接続を開始する。
- 接続が完了すると、 `onopen` に渡した関数がコールバックされる。
- `send()` メソッドでサーバーにメッセージを送信する。
- サーバーからメッセージが送られると、 `onmessage` に設定した関数がコールバックされる。
    - 引数には [MessageEvent オブジェクト](http://msdn.microsoft.com/ja-jp/library/ie/ff974343%28v=vs.85%29.aspx) が渡される。

#サーバーからクライアントにメッセージを送信する
サーバーからクライアントにメッセージを送る方法は、大きく２つある。
１つは Hello World のところの実装のように、 `@OnMessage` でアノテートしたメソッドの戻り値を使う方法。
もう１つは、 `RemoteEndpoint` を使う方法。

```java:
package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class SampleWebSocket {
    
    @OnMessage
    public void echo(String message, Session session) throws IOException {
        session.getBasicRemote().sendText(message);
    }
}
```

- `javax.websocket.Session` を引数に追加して、 `getBasicRemote()` メソッドで `RemoteEndpoint` のインスタンスを取得し、 `sendText()` メソッドでクライアントにメッセージを送信する。

##非同期でメッセージを送信する
`getBasicRemote()` で取得できる `RemoteEndpoint` は、メッセージの送信が完了するまで処理を待機する。
終了を待ちたくない場合は、 `getAsyncRemote()` で `RemoteEndpoint` を取得する。

```java:
package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/echo")
public class SampleWebSocket {
    
    @OnMessage
    public void echo(String message, Session session) throws IOException {
        session.getAsyncRemote().sendText(message);
    }
}
```

#接続中の全てのクライアントにメッセージを送信する
##実装
```java:
package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/broadcast")
public class SampleWebSocket {
    
    @OnMessage
    public void broadcast(String message, Session session) throws IOException {
        session.getOpenSessions().forEach(s -> {
            s.getAsyncRemote().sendText(message);
        });
    }
}
```

```html:
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <input type="text" id="message" />
    <button id="send">Send</button>
    
    <hr>
    
    <span id="receive"></span>
  </body>
</html>
```

```js:
$(function() {
    $('#send').attr('disabled', true);
    var ws = new WebSocket('ws://localhost:8080/websocket/broadcast');
    
    ws.onopen = function() {
        $('#send').attr('disabled', false);
    };
    
    ws.onmessage = function(receive) {
        $('#receive').text(receive.data);
    };
    
    $('#send').on('click', function() {
        var sendMessage = $('#message').val();
        ws.send(sendMessage);
    });
});
```

##動作確認
IE, Chrome, Firefox でページを開いて、 IE でテキストを入力、ボタンをクリックする。

![websocket.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/cd7ef833-ae83-ec7a-b5c4-86d3f2cd3c16.jpeg)

すると、 Chrome, Firefox に IE で入力した文字が出力される。

##説明
- `Session.getOpenSessions()` で、現在接続中の全てのセッションを取得できる。

##問題点
この実装方法で、ブロードキャストは問題なく実装できる。

しかし、この実装には無駄が多い。
クライアントに送信するデータは全て同じ内容だが、送信するための形式にデータを変換する処理が毎回行われている。

同じデータは使いまわせるようにすると効率が良いか、現状 WebSocket の API にはこれを実現するものが定義されていない。

リファレンス実装である Tyrus を使っていれば、以下のように実装することでこの無駄を回避できる。

```java:
@OnMessage
public void onMessage(Session s, String m) {
  ((TyrusSession) s).broadcast(m);
}
```

今後のバージョンアップで改善されるかもしれない。

**参考**
- [Oracle Blogs 日本語のまとめ: [Java] Optimized WebSocket broadcast](http://orablogs-jp.blogspot.jp/2013/12/optimized-websocket-broadcast.html)

#イベントハンドリング
##実装
```java:
package sample.websocket;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/event")
public class SampleWebSocket {
    
    @OnOpen
    public void onOpen(Session session) {
        System.out.println("open : " + session.getId());
    }
    
    @OnClose
    public void onClose(Session session) {
        System.out.println("close : " + session.getId());
    }
    
    @OnError
    public void onError(Session session, Throwable cause) {
        System.out.println("error : " + session.getId() + ", " + cause.getMessage());
    }
}
```

```js:
new WebSocket('ws://localhost:8080/websocket/event');
```

##動作確認
ブラウザで `http://localhost:8080/websocket/` を表示。

```text:サーバー側コンソール出力
open : 0
```

F5 で画面を再描画。

```text:サーバー側コンソール出力
close : 0
open : 1
```

ブラウザを閉じる。

```text:サーバー側コンソール出力
error : 1, java.util.concurrent.ExecutionException: java.io.IOException: 確立された接続がホスト コンピューターのソウトウェアによって中止されました。
close : 1
```

##説明
- `@OnOpen` で接続開始時、 `@OnClose` で接続終了時、 `@OnError` で通信エラー時の処理をハンドリングできる。
- WebSocket のセッションと HttpServletSession は完全に別物。
- ブラウザが再描画されれば JavaScript も実行され直すので、前の接続（セッション）は切断される。

#パスパラメータを使用する
##実装
```java:
package sample.websocket;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/pathparam/{string}/{number}")
public class SampleWebSocket {

    @OnOpen
    public void open(@PathParam("string") String string, @PathParam("number") int number) {
        System.out.printf("OnOpen : string=%s, number=%d%n", string, number);
    }
    
    @OnMessage
    public void message(String message, @PathParam("string") String string, @PathParam("number") int number) {
        System.out.printf("Message : message=%s, string=%s, number=%d%n", message, string, number);
    }
    
    @OnClose
    public void close(@PathParam("string") String string, @PathParam("number") int number) {
        System.out.printf("Close : string=%s, number=%d%n", string, number);
    }
}
```

```html:
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="hoge">hoge</button>
    <button id="fuga">fuga</button>
  </body>
</html>
```

```js:
$(function() {
    $('#hoge').on('click', function() {
        var ws = new WebSocket('ws://localhost:8080/websocket/pathparam/hoge/11');
        ws.onopen = function() {
            ws.send('hoge message');
            ws.close();
        };
    });
    
    $('#fuga').on('click', function() {
        var ws = new WebSocket('ws://localhost:8080/websocket/pathparam/fuga/22');
        ws.onopen = function() {
            ws.send('fuga message');
            ws.close();
        };
    });
});
```

##動作確認
ブラウザでページを開いて、「hoge」ボタンをクリック。

```text:サーバー側コンソール出力
OnOpen : string=hoge, number=11
Message : message=hoge message, string=hoge, number=11
Close : string=hoge, number=11
```

「fuga」ボタンをクリック。

```text:サーバー側コンソール出力
OnOpen : string=fuga, number=22
Message : message=fuga message, string=fuga, number=22
Close : string=fuga, number=22
```

##説明
- `@ServerEndpoint` の `value` 属性に指定するパスに、 `{パラメータ名}` という形式でパスパラメータを定義することができる。
- パスパラメータは、 `@OnOpen`, `@OnMessage`, `@OnClose`, `@OnError` などの各メソッドで引数として受け取ることができる。
- メソッド引数には、 `@PathParam("パラメータ名")` という形で受け取るパラメータを指定する。
- 基本は `String` だが、 `int` などのプリミティブ型で受け取ることも可能。

#接続を閉じる
##サーバー側から閉じる
###実装
```java:
package sample.websocket;

import java.io.IOException;

import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/close")
public class SampleWebSocket {
    
    @OnMessage
    public void close(String message, Session session) throws IOException {
        session.close();
    }
}
```

```js:
var ws = new WebSocket('ws://localhost:8080/websocket/close');

ws.onopen = function() {
    ws.send(null);
}

ws.onclose = function(closeEvent) {
    console.log('code = ' + closeEvent.code + ', reason = ' + closeEvent.reason);
};
```

###動作確認
```text:ブラウザ側コンソール出力
code = 1000, reason =  
```

##ブラウザ側から閉じる
###実装
```js:
var ws = new WebSocket('ws://localhost:8080/websocket/close');

ws.onopen = function() {
    ws.close();
}
```

```java:
package sample.websocket;

import java.io.IOException;

import javax.websocket.CloseReason;
import javax.websocket.OnClose;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/close")
public class SampleWebSocket {
    
    @OnClose
    public void onClose(Session session, CloseReason reason) throws IOException {
        System.out.println("code = " + reason.getCloseCode().getCode() + ", reason = " + reason.getReasonPhrase());
    }
}
```

###動作確認
```text:サーバー側コンソール出力
code = 1000, reason = null
```

##説明
- サーバーから切断する場合は、 `Session#close()` メソッドを使用する。
- クライアントから切断する場合は、 `WebSocket#close()` メソッドを使用する。

#ServerEndpoint のインスタンスはセッションごとに生成される
##実装
```java:
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/instance")
public class SampleWebSocket {
    
    @OnMessage
    public void message(String message) {
        System.out.println(this.hashCode());
    }
}
```

```html:
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="send">Send</button>
  </body>
</html>
```

```js:
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/instance');

    ws.onopen = function() {
        $('#send').on('click', function() {
            ws.send('message');
        });
    }
});
```

##動作確認
ブラウザ（Chrome）を開き、「Send」ボタンを何回かクリック。

```text:サーバー側コンソール出力
1962593639
1962593639
1962593639
1962593639
```

新しいタブでページを開き、そちらで「Send」ボタンを何回かクリック。

```text:サーバー側コンソール出力
2050833800
2050833800
2050833800
2050833800
```

F5で画面を再描画してから、「Send」ボタンを何回かクリック。

```text:サーバー側
1334674039
1334674039
1334674039
```

##説明
ServerEndpoint のインスタンスは、セッションごと（コネクションごと）に作成される。

１つのセッションの中では、常に同じ ServerEndpoint のインスタンスが利用される。
また、 ServerEndpoint のインスタンスは常に１つのスレッドとだけ関連付けられる。

つまり、セッションごとの情報を保持しておきたい場合は、 ServerEndpoint のインスタンスフィールドに保持しておけば良い。

> Note:
> As opposed to servlets, WebSocket endpoints are instantiated multiple times.
> （略）
> Each instance is associated with one and only one connection. This facilitates keeping user state for each connection and makes development easier, because there is only one thread executing the code of an endpoint instance at any given time.

[The Java EE 7 Tutorial:Creating WebSocket Applications in the Java EE Platform | Java EE Documentation](https://docs.oracle.com/javaee/7/tutorial/doc/websocket002.htm)

#サーバーから自発的にメッセージを送信する
ここまでの実装方法だと、サーバー側から自発的にクライアントにメッセージを送ることができない。

サーバー側から自発的にメッセージを送信するには、 ServerEndpoint の static フィールドに Session を保持しておき、 static メソッドを介して Session にアクセスする。

##実装
```java:SampleWebSocket.java
package sample.websocket;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.websocket.OnClose;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint("/broadcast")
public class SampleWebSocket {
    
    private static final Queue<Session> sessions = new ConcurrentLinkedQueue<>();
    
    static {
        ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
        service.scheduleWithFixedDelay(SampleWebSocket::broadcast, 5, 5, TimeUnit.SECONDS);
    }
    
    @OnOpen
    public void connect(Session session) {
        sessions.add(session);
    }
    
    @OnClose
    public void remove(Session session) {
        sessions.remove(session);
    }
    
    public static void broadcast() {
        Date now = new Date();
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        
        sessions.forEach(session -> {
            session.getAsyncRemote().sendText("Broadcast : " + formatter.format(now));
        });
    }
}
```

```html:
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <span id="message"></span>
  </body>
</html>
```

```js:
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/broadcast');
    
    ws.onmessage = function(e) {
        $('#message').text(e.data);
    };
});
```

##動作確認
ブラウザをいくつか立ち上げて、 `http://lcoalhost:8080/broadcast` を開く。

最初のアクセスから５秒ほど経過すると、各ブラウザに現在時刻が出力される。

![websocket.JPG](https://qiita-image-store.s3.amazonaws.com/0/28302/2330d1eb-8f3f-7469-f870-68d456864727.jpeg)

以後、５秒ごとに時刻の表示が更新される。

##説明
- `@OnOpen` のときに `Session` を static 宣言した `ConcurrentLinkedQueue` に格納している。
- また、 `@OnClose` のときに `Session` をキューから削除している。

自作の static フィールドに保存して管理するというのが、なんだか大丈夫なのかなと不安になるけど、 [公式のチュートリアル](https://docs.oracle.com/javaee/7/tutorial/doc/websocket011.htm#BABGCEHE) がこうなっているのだから、きっと大丈夫なのだろう。

ちなみに、 JavaEE 環境なら CDI の Event を使ってもうちょっとスマート？に実装する方法が Stack Overflow で紹介されている。

[java - How to get an existing websocket instance - Stack Overflow](http://stackoverflow.com/questions/18481597/how-to-get-an-existing-websocket-instance)

#メッセージを Java オブジェクトにマッピングするための仕組みを利用する
##デコーダー（メッセージ→Javaオブジェクト）
###実装
```java:Hoge.java
package sample.websocket;

public class Hoge {
    
    public int id;
    public String name;
    
    @Override
    public String toString() {
        return "Hoge [id=" + id + ", name=" + name + "]";
    }
}
```

```java:HogeDecoder.java
package sample.websocket;

import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

public class HogeDecoder implements Decoder.Text<Hoge> {

    @Override
    public void init(EndpointConfig config) {
        System.out.println("init");
    }

    @Override
    public boolean willDecode(String s) {
        System.out.println("willDecode");
        return true;
    }

    @Override
    public Hoge decode(String s) throws DecodeException {
        System.out.println("decode");
        
        String[] tokens = s.split(":");
        
        Hoge hoge = new Hoge();
        hoge.id = Integer.parseInt(tokens[0]);
        hoge.name = tokens[1];
        
        return hoge;
    }
    
    @Override
    public void destroy() {
        System.out.println("destroy");
    }
}
```

```java:SampleWebSocket.java
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/decode", decoders=HogeDecoder.class)
public class SampleWebSocket {
    
    @OnMessage
    public void message(Hoge hoge) {
        System.out.println(hoge);
    }
}
```

```html:
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="send">Send</button>
    <button id="close">Close</button>
  </body>
</html>
```

```js:
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/decode');
    
    ws.onopen = function(e) {
        $('#send').on('click', function() {
            ws.send('12:Hoge');
        });

        $('#close').on('click', function() {
            ws.close();
        });
    };
});
```

###動作確認
Web ブラウザでページを表示。

```text:サーバー側コンソール出力
init
```

「Send」ボタンをクリック。

```text:サーバー側コンソール出力
willDecode
decode
Hoge [id=12, name=Hoge]
```

もう一回クリック。

```text:サーバー側コンソール出力
willDecode
decode
Hoge [id=12, name=Hoge]
```

「Close」ボタンをクリック。

```text:サーバー側コンソール出力
destroy
```

###説明
- `Decoder` を実装したクラスを作成し、 `@ServerEndpoint` の `decoders` に指定する。
- `Decoder` にはテキストを変換する `Text<T>` や、バイナリデータを変換する `Binary<T>` というサブインターフェースが用意されており、実際はこれらを実装したクラスを作成する。
- デコーダーが指定されていると、クライアントからのメッセージは一旦このデコーダーに渡される。
- デコーダーでは、以下のメソッドを実装する。

|メソッド|説明|
|:--|:--|
|init|初期化処理。セッションが確立されたときに１度だけ実行される。|
|willDecode|メッセージをデコードするかどうかを判定する。falseを返した場合、 `decode()` メソッドは実行されない。|
|decode|メッセージを Java オブジェクトに変換するデコード処理を実装する。|
|destroy|セッションが破棄されるときに１度だけ実行される。|

- `@OnMessage` でアノテートした ServerEndpoint のメソッドには、デコーダーが作成した Java オブジェクトが引数に渡される。

##エンコーダー（Javaオブジェクト→メッセージ）
###実装
```java:HogeEncoder.java
package sample.websocket;

import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class HogeEncoder implements Encoder.Text<Hoge>{

    @Override
    public void init(EndpointConfig config) {
        System.out.println("init");
    }

    @Override
    public String encode(Hoge hoge) throws EncodeException {
        System.out.println("encode");
        return hoge.id + ":" + hoge.name;
    }

    @Override
    public void destroy() {
        System.out.println("destroy");
        
    }
}
```

```java:SampleWebSocket.java
package sample.websocket;

import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/encode", encoders=HogeEncoder.class)
public class SampleWebSocket {
    
    @OnMessage
    public Hoge message(String message) {
        System.out.println("message");
        Hoge hoge = new Hoge();
        hoge.id = 20;
        hoge.name = "HOGE";
        
        return hoge;
    }
}
```

```js:
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/encode');
    
    ws.onopen = function() {
        $('#send').on('click', function() {
            ws.send(null);
        });

        $('#close').on('click', function() {
            ws.close();
        });
    };
    
    ws.onmessage = function(e) {
        console.log(e.data);
    };
});
```

###動作確認
ブラウザでページを表示。

```text:サーバー側コンソール出力
init
```

「Send」ボタンをクリック。

```text:サーバー側コンソール出力
message
encode
```

```text:クライアント側コンソール出力
20:HOGE
```

「Close」ボタンをクリック。

```text:サーバー側コンソール出力
destroy
```

###説明
- デコーダーの逆。変換の向きが逆になったこと以外、特に大きな違いはない。

#おまけ
Encoder と Decoder は、そのままだとパラメータで使う型の数だけ作成しないといけないため、ちょっとイケてない。

ということで、 json と任意の型とを変換する Encoder と Decoder を実装してみた。

##build.gradle
```groovy:build.gradle
dependencies {
    compile 'com.fasterxml.jackson.core:jackson-databind:2.4.3'
    providedCompile 'javax.websocket:javax.websocket-api:1.1'
}
```

json の変換には Jackson を利用する。

##実装
```java:JsonConverter.java
package sample.websocket;

import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Stream;

import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.PathParam;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonConverter implements Encoder.Text<Object>, Decoder.Text<Object> {
    
    public static final String ENDPOINT_CLASS = "json.endpoint.class";
    
    private ObjectMapper mapper = new ObjectMapper();
    private Map<String, Object> userProperties;
    private Class<?> messageClass;
    
    @Override
    public void init(EndpointConfig config) {
        this.userProperties = config.getUserProperties();
    }

    @Override
    public Object decode(String s) throws DecodeException {
        try {
            Class<?> messageClass = this.getMessageClass();
            return this.mapper.readValue(s, messageClass);
        } catch (IOException e) {
            throw new DecodeException(s, "failed to decode message.", e);
        }
    }
    
    private Class<?> getMessageClass() {
        if (this.messageClass == null) {
            this.initMessageClass();
        }
        
        return this.messageClass;
    }
    
    private void initMessageClass() {
        Class<?> endpointClass = (Class<?>)userProperties.get(ENDPOINT_CLASS);
        
        Method onMessageMethod = this.findOnMessageMethod(endpointClass);
        Parameter messageParameter = this.findMessageParameter(onMessageMethod);
        
        this.messageClass = messageParameter.getType();
    }
    
    private Method findOnMessageMethod(Class<?> endpointClass) {
        return this.findFromArray(endpointClass.getMethods(), this::isOnMessageMethod, "method annotated by @OnMessage is not found.");
    }
    
    private boolean isOnMessageMethod(Method method) {
        return method.isAnnotationPresent(OnMessage.class);
    }
    
    private Parameter findMessageParameter(Method onMessageMethod) {
        return this.findFromArray(onMessageMethod.getParameters(), this::isMessageParameter, "message parameter is not found.");
    }
    
    private boolean isMessageParameter(Parameter parameter) {
        return this.isNotSession(parameter) && this.isNotPathParam(parameter);
    }

    private boolean isNotSession(Parameter parameter) {
        return !Session.class.isAssignableFrom(parameter.getType());
    }

    private boolean isNotPathParam(Parameter parameter) {
        return !parameter.isAnnotationPresent(PathParam.class);
    }
    
    private <T> T findFromArray(T[] array, Predicate<T> condition, String exceptionMessage) {
        return Stream.of(array)
                     .filter(condition)
                     .findFirst()
                     .orElseThrow(() -> new RuntimeException(exceptionMessage));
    }

    @Override
    public String encode(Object object) throws EncodeException {
        try {
            return this.mapper.writeValueAsString(object);
        } catch (JsonProcessingException e) {
            throw new EncodeException(object, "failed to encode message.", e);
        }
    }

    @Override
    public boolean willDecode(String s) {
        return true;
    }

    @Override public void destroy() {/*no use*/}
}
```

```java:
package sample.websocket;

import javax.websocket.EndpointConfig;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value="/json", encoders=JsonConverter.class, decoders=JsonConverter.class)
public class SampleWebSocket {

    @OnOpen
    public void open(EndpointConfig config) {
        config.getUserProperties().put(JsonConverter.ENDPOINT_CLASS, SampleWebSocket.class);
    }

    @OnMessage
    public Hoge message(Hoge message, Session session) {
        System.out.println("message = " + message);

        Hoge hoge = new Hoge();
        hoge.id = 33;
        hoge.name = "fuga";

        return hoge;
    }
}
```

```html:
<!DOCTYPE html>
<html>
  <head>
    <title>WebSocket Sample</title>
    <script src="jquery.min.js"></script>
    <script src="script.js"></script>
  </head>
  <body>
    <button id="send">Send</button>
    <button id="close">Close</button>
  </body>
</html>
```

```js:
$(function() {
    var ws = new WebSocket('ws://localhost:8080/websocket/json');
    
    ws.onopen = function() {
        $('#send').on('click', function() {
            var message = {id: 15, name: "Hoge"};
            ws.send(JSON.stringify(message));
        });

        $('#close').on('click', function() {
            ws.close();
        });
    };
    
    ws.onmessage = function(e) {
        console.log(e.data);
    };
});
```

##動作確認
ブラウザでページを表示して、「Send」ボタンをクリック。

```text:サーバー側コンソール出力
message = Hoge [id=15, name=Hoge]
```

```text:クライアント側コンソール出力
{"id":33,"name":"fuga"}
```

##説明
- `EndpointConfig#getUserProperties()` で取得できる Map （`UserProperties`）を介して、 ServerEndpoint の型の情報をやり取りしている。
- `SampleWebSocket` の `open()` メソッド内で、 ServerEndpoint の型を `UserProperties` に設定する。
- `JsonConverter` 側は、 `init()` メソッドで `UserProperties` を取得してインスタンス変数に保存しておく。
    - `SampleWebSocket` の `open()` メソッドより `JsonConverter` の `init()` メソッドが先に呼ばれる。
    - このため、この時点では `UserProperties` に型情報は保存されていない。
- そして、 `decode()` メソッドが呼ばれたときに変換後の型情報をリフレクションで取得し、 Jackson でデコードしている。

これで `JsonConverter` は使い回ししやすくなるはず。

#参考
- [JSR 356―Java標準のWebSocket API - Programming Studio](http://www.coppermine.jp/docs/programming/2013/12/jsr356-fundamental.html)
- [Tyrus 1.8.3 User Guide](https://tyrus.java.net/documentation/1.8.3/user-guide.html)
- [WebSocket API (Windows)](http://msdn.microsoft.com/ja-jp/library/ie/hh673567%28v=vs.85%29.aspx)