JavaEE」タグアーカイブ

WebSocketにてJSON形式のデータをJavaFXとやり取りしてみる。の4

先ほどに続いて送受信部分

送信処理

  1. 画面の送信ボタンからSingleController#handleSendActionが呼ばれる。
  2. SingleController#handleSendActionから@ClientEndpointのsendメソッド(自分で作った普通のメソッド)を呼び出す。
  3. sendメソッド内でSession.getAsyncRemote().sendObject(dataObj)を呼び出す
  4. encodersに指定してあるClientEncoderクラスのencodeメソッドが呼び出される。
  5. encodeメソッドでエンコードされたデータ(JSON形式の文字列)がServerEndpointに送信される。

エコープログラムなのでServerEndpointで受信されたデータは(若干加工され)ClientEndpointに戻される。そのため、続けて受信処理が走る。

 

受信処理

  1. サーバからのデータがPushされる。
  2. @ClientEndpointのdecodersに指定してあるClientDecoderクラスのwillDecodeメソッドが呼び出され入力値がチェックされる。
  3. willDecodeの結果がtrueならば続けてdecodeメソッドが呼び出されてジェネリクスで指定した型(ClientData)にデコードされる。
  4. デコードされたデータを引数に@ClientEndpointの@OnMessageが呼び出される。ここはJavaFXのスレッド配下ではなく、WebSocketのスレッドに移っている。そのため、JavaFXのコントロールを直接操作することはできない。
  5. @OnMessageの中でPlatform.runLaterを呼び、JavaFXの画面操作(接続時処理ViewObj#write)の呼び出しを登録(?)する。
  6. JavaFXスレッドに戻り、JavaFXの画面操作が実行される。

(コントローラーの関連する部分)

public class SingleController implements Initializable {
    @FXML
    private void handleSendAction(ActionEvent event) {
        System.out.println("SingleController#handleSendAction");
        wsClient.send(msgInput.getText(), sess);
    }
}

(ClientEndpointの関連する部分)

@ClientEndpoint( 
   decoders = { ClientDecoder.class }, 
   encoders = { ClientEncoder.class })
public class WSJsonSingleClient {

    @OnMessage
    public void onMessage(ClientData dataObj) {
        System.out.println("WSJsonSingleClient#onMessage");
        final ClientData channeled = dataObj;
        Platform.runLater(new Runnable() {
            @Override
            public void run() {
                viewObj.write(
                        channeled.getSessionId(), 
                        channeled.getMessageTimeString(), 
                        channeled.getMessage());
            };
        });
    }

    public void send(String text,Session sess) {
        System.out.println("WSJsonSingleClient#send");
        ClientData dataObj = new ClientData(text);
        sess.getAsyncRemote().sendObject(dataObj);
    }
}

(ClientEncoder)

public class ClientEncoder implements Encoder.Text<ClientData> {

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

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

    @Override
    public String encode(ClientData paramData) throws EncodeException {
        System.out.println("ClientEncoder#encode");
        JsonObject model = Json.createObjectBuilder()
                   .add("message", paramData.getMessage())
                   .add("messageTime",paramData.getMessageTime())
                   .build();
        return model.toString();
    }
}

(ClientDecoder)

public class ClientDecoder implements Decoder.Text<ClientData> {

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

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

    @Override
    public ClientData decode(String inputString) throws DecodeException {
        System.out.println("ClientDecoder#decode");
        try{
            JsonObject jsonObject = Json.createReader(new StringReader(inputString)).readObject();
            return  new ClientData(jsonObject);
        } catch(Exception e){
            e.printStackTrace();
            throw new DecodeException(inputString,"ClientDecoder#decode失敗", (Throwable)e);
        }
    }

    @Override
    public boolean willDecode(String inputString) {
        try {
            System.out.println("ClientDecoder#willDecode");
            Json.createReader(new StringReader(inputString)).readObject();
            return true;
        } catch (JsonException ex) {
            ex.printStackTrace();
            return false;
        }
    }
}

github:https://github.com/epea/test01/tree/WF_JSON_FX_SINGLE

WebSocketにてJSON形式のデータをJavaFXとやり取りしてみる。の2

先ほどの続き

前提条件

  • JavaFXのコントロール(ラベルとかボタンとか)はJavaFXのスレッド配下からでないと原則(※1)操作できない(と思う)
  • WebSocketはプログラム本体とは別のスレッドでデータの送受信を行う。
  • WebSocketのスレッド内からPlatform#runLaterを呼ぶことで、JavaFXのスレッド配下で行わせたい処理を登録できる。
  • 今の実装だと、接続状況の排他制御がきちんとされていないので、「サーバからの切断」と「クライアントからの起動」のシーケンスが重なったときとかにうまく動かないケースがでてくる。(はず)

※1 こちらにあるようにコントロールに結びついたObservableListを別スレッドから操作した場合は、その操作が画面に反映される。ラベルとかボタンとかのコントロールで試した範囲ではJavaFXのスレッドかどうかチェックにかかってうまくいかなかったがやりようはあるかもしれない。

ApplicationクラスとFXML

Applicationクラスは起動するだけ。

public class WSJsonSingle extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("SingleJson.fxml"));
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }

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

 

FXMLはfx:idふって、onActionを紐付けている程度。

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane prefHeight="363.0" prefWidth="502.0" xmlns:fx="http://javafx.com/fxml" fx:controller="jp.co.epea.wsclient.SingleController">
  <children>
    <Label alignment="CENTER" contentDisplay="CENTER" prefHeight="43.0" prefWidth="502.0" text="JSON送受信" />
    <VBox layoutY="43.0" prefHeight="331.0" prefWidth="502.0">
      <children>
        <HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0">
          <children>
            <Button fx:id="startButton" mnemonicParsing="false" onAction="#handleStartButtonAction" text="開始" />
            <Button fx:id="stopButton" mnemonicParsing="false" onAction="#handleStoptButtonAction" text="終了" />
            <Label fx:id="statusLabel" text="ステータス" />
          </children>
        </HBox>
        <HBox prefHeight="100.0" prefWidth="200.0">
          <children>
            <TextField fx:id="msgInput" editable="false" prefWidth="200.0" />
            <Button fx:id="sendButton" mnemonicParsing="false" onAction="#handleSendAction" text="送信" />
          </children>
        </HBox>
        <VBox prefHeight="200.0" prefWidth="100.0">
          <children>
            <HBox prefHeight="100.0" prefWidth="200.0">
              <children>
                <Label text="発言者:" />
                <Label fx:id="nameLabel" text="発言者初期値(起動時に変更)" />
              </children>
            </HBox>
            <HBox prefHeight="100.0" prefWidth="200.0">
              <children>
                <Label text="発言日時:" />
                <Label fx:id="timeLabel" text="発言日時初期値(起動時に変更)" />
              </children>
            </HBox>
            <HBox prefHeight="100.0" prefWidth="200.0">
              <children>
                <Label text="発言内容:" />
                <Label fx:id="msgLabel" text="発言内容初期値(起動時に変更)" />
              </children>
            </HBox>
          </children>
        </VBox>
      </children>
    </VBox>
  </children>
</AnchorPane>

起動処理

WSJsonSingle#mainから。

画面項目の初期化と、ClientEndpointのインスタンス化(まだ接続しない)。

public class SingleController implements Initializable {

    private WSJsonSingleClient wsClient = null;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // コントロールをまとめたクラスを作って
        ViewObj obj = new ViewObj(nameLabel, timeLabel, msgLabel, statusLabel,
                msgInput, startButton, stopButton, sendButton);
        // ClientEndpointに渡してインスタンス化
        wsClient = new WSJsonSingleClient(obj);
    }
}

public class ViewObj {
    private Label nameLabel;
   // 略
    ViewObj(Label nameLabel, Label timeLabel, Label msgLabel,
            Label statusLabel, TextField msgInput, Button startButton,
            Button stopButton, Button sendButton) {
        super();
        // 略
        // コントロール類の初期化(項目設定とか、ボタンのdisable周りとか)
        this.init();
    }

    private void init(){
        nameLabel.setText("");
        // 略
    }
}

@ClientEndpoint( decoders = { ClientDecoder.class }, encoders = { ClientEncoder.class })
public class WSJsonSingleClient {

    private ViewObj viewObj;

    public WSJsonSingleClient(ViewObj viewObj) {
        this.viewObj = viewObj;
    }
    // 略
}

github:https://github.com/epea/test01/tree/WF_JSON_FX_SINGLE

続き

WebSocketにてJSON形式のデータをJavaFXとやり取りしてみる

Glassfish4とJavaFX間でJSON形式のデータをWebsocketにてやり取りしてみたのでメモ。

何ページかに分かれているけど実際にJSONのハンドリングをしている箇所はこちら

サーバサイドはこの前HTMLとやり取りしたものとほぼ同じ。

大まかな動きは以下。なお、表示はリスト形式ではなくてラベルの中身を置換していく形式(表示以外はやっていることはHTMLの時とほぼ一緒)

  1. HTMLからJSONで入力テキストと現在日時を送信。
  2. GlassfishのWebsocketで受信(&Decode)
  3. 受け取ったデータに情報(とりあえずユーザ情報代わりのセッションID)を付与し繋がっているクライアント(JavaFX&この前作ったHTML)全体にJSON形式で返信
  4. JavaFX(またはHTML)で受け取ったデータを表示

データ形式はこの前と同じ。(なので、クライアントはこの前のHTMLとJavaFXで併用できる。)

(JavaFX->glassfishのデータ形式)
{"message":"メッセージ","messageTime":時間(ミリ秒)}

(glassfish->JavaFXのデータ形式)
{"message":"メッセージ","sessionId":"セッションID","messageTime":"HH:mm:ss"}

斜体は変数

Maven

必要な依存関係を追加。(動作確認はeclipse上のM2E)

    <dependencies>
       <!-- JavaFX 2 -->
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>javafx</artifactId>
            <version>2.2</version>
            <scope>system</scope>
            <systemPath>${javafx2.home}</systemPath>
        </dependency>
        <!-- Websocket (JSR-356) -->
        <!-- TYRUS-210の対応で1.2にあげている -->
        <dependency>
            <groupId>org.glassfish.tyrus</groupId>
            <artifactId>tyrus-client</artifactId>
            <version>1.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.tyrus</groupId>
            <artifactId>tyrus-container-grizzly</artifactId>
            <version>1.2</version>
            <scope>compile</scope>
        </dependency>
        <!-- JSON Processing (JSR-353) -->
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.2 </version>
        </dependency>
    </dependencies>

画面フロー

起動時

開始ボタンしか押せない状態。裏ではWebSocket用のClientEndPointをインスタンス化している。

initial

 

開始ボタンクリック後(の入力欄に文字入力した状態)

入力欄や送信ボタン・終了ボタンが使えるようになる。

bef_send

送信ボタンクリック後

入力メッセージと時間がサーバに送信される。そしてデータを受信したサーバにて加工(といってもセッションID文字列を付加しただけ)されたデータがクライアントに送られ、そのデータがラベルに表示される。

aft_send

 

終了ボタンクリック後

開始ボタンしか押せない状態になる。

closed

 

github:https://github.com/epea/test01/tree/WF_JSON_FX_SINGLE

続き。

TYRUS-210の対応

昨日書いたDecoder.TextのwillDecodeが2度呼ばれる問題の対応(下参照)、試したら動いた。

Tyrus 1.2は既にGlassFishに統合・組み込まれています。
Nightly Buildのダウンロードもしくは手作業で新しいTyrusにアップグレードすることができます
(手作業の場合、全てのTyrusのjarファイルを置き換えて下さい)。
  1. Nightly Buildから最新のバージョンをダウンロード(8/11時点で glassfish-4.0.1-b02-08_10_2013.zip
  2. ダウンロードしたzipのglassfish4\glassfish\modulesに入っているtruis*.jarをインストールパスの同じところに上書き(自分の環境ではC:\develop\middles\glassfish4\glassfish\modules)

以上でeclipseから動かしてみるとDecoder.TextのwillDecodeが2度呼ばれることは無くなっていた。Eclipse上でのMaven指定等は特に修正する必要はなかった。

 

他にもいくつかバグが修正されているのでそれも直っているはず。

 

Decoder.TextのwillDecodeが2度呼ばれる(TYRUS-210)

WebSocketのServerEndpointとClientEndpointでDecoder.Text<T>を使っていたらwillDecodeが2度呼ばれていた。

Tyrusのバグ(TYRUS-210)とのこと。(1.2で修正済み)

(ServerEndpoint)
情報: TestDecoder#init
情報: TestDecoder#willDecode
情報: TestDecoder#willDecode
情報: TestDecoder#decode

(ClientEndpoint)
ClientDecoder#init
ClientDecoder#willDecode
ClientDecoder#willDecode
ClientDecoder#decode
WSJsonSingleClient#onMessage

クライアントサイドは新しいバージョンをpomに指定して修正されたことを確認。

   <dependency>
   <groupId>org.glassfish.tyrus</groupId>
   <artifactId>tyrus-client</artifactId>
   <version>1.2</version>
   <scope>compile</scope>
  </dependency>
  <dependency>
   <groupId>org.glassfish.tyrus</groupId>
   <artifactId>tyrus-container-grizzly</artifactId>
   <version>1.2</version>
   <scope>compile</scope>
  </dependency>

ClientDecoder#init
ClientDecoder#willDecode
ClientDecoder#decode
WSJsonSingleClient#onMessage

サーバサイドのGlassfishも下のように対応されているとのこと。(参照元)

Tyrus 1.2は既にGlassFishに統合・組み込まれています。
Nightly Buildのダウンロードもしくは手作業で新しいTyrusにアップグレードすることができます
(手作業の場合、全てのTyrusのjarファイルを置き換えて下さい)。

サーバサイドのMavenで対応されているものはぱっとみ見つからなかった。->実装の修正なのでサーバランタイムを変えたらいい? -> 変えたら治った

WebSocketのエンコード・デコード時のエラー

先日書いたWebsocketでJSONデータをやり取りしたブログだけど、エラー出力をきちんとしていなくてはまった。

Decoder.Text<T>のdecodeメソッド内でExceptionが発生した場合に、明示的にログ・スタックトレースの出力をしておかないと特にスタックトレース等が出力されないでdecodeメソッド(とそれから呼ばれる@OnMessage)が終了してしまう。(コンソールにエラーがつらつら出るようなことは無い。)

そのため、下みたいな感じで明示的にエラーあったことを出力してやらないとわかりにくい。

    @Override
    public TestData decode(String inputString) throws DecodeException {
        try{
            JsonObject jsonObject = Json.createReader(new StringReader(inputString)).readObject();
            return  new TestData(jsonObject);
        } catch(Exception e){
            DecodeException de = 
                    new DecodeException(inputString,"decode失敗", (Throwable)e);
            Logger.getLogger(TestDecoder.class.getName()).log(
                    Level.SEVERE, null, de);
            throw de;
        }
    }

Encoder.Text<T>のencodeメソッドも似たような話がある。

    @Override
    public String encode(TestData paramData) throws EncodeException {
        try{
            JsonObject model = Json.createObjectBuilder()
                       .add("message", paramData.getMessage())
                       .add("sessionId",paramData.getSessionId())
                       .add("messageTime", formatDate(paramData.getMessageTime()))
                       .build();
            return model.toString();
        } catch( Exception e ){
            EncodeException ee =  
                    new EncodeException(paramData, "encode失敗",  (Throwable)e);
            Logger.getLogger(TestEncoder.class.getName()).log(
                    Level.SEVERE, null, ee);
            throw ee;
        }
    }

一応decodeでエラーを発生させたときのログサンプル

重大: javax.websocket.DecodeException: decode失敗
    at jp.co.epea.first.json.TestDecoder.decode(TestDecoder.java:33)
    at jp.co.epea.first.json.TestDecoder.decode(TestDecoder.java:1)
    at org.glassfish.tyrus.core.EndpointWrapper.decodeCompleteMessage(EndpointWrapper.java:278)
    at org.glassfish.tyrus.core.SessionImpl.notifyMessageHandlers(SessionImpl.java:386)
    at org.glassfish.tyrus.core.EndpointWrapper.onMessage(EndpointWrapper.java:495)
    at org.glassfish.tyrus.server.TyrusEndpoint.onMessage(TyrusEndpoint.java:174)
    at org.glassfish.tyrus.websockets.DefaultWebSocket.onMessage(DefaultWebSocket.java:156)
    at org.glassfish.tyrus.websockets.frametypes.TextFrameType.respond(TextFrameType.java:66)
    at org.glassfish.tyrus.websockets.DataFrame.respond(DataFrame.java:102)
    at org.glassfish.tyrus.servlet.TyrusHttpUpgradeHandler.onDataAvailable(TyrusHttpUpgradeHandler.java:113)
    at org.apache.catalina.connector.InputBuffer$ReadHandlerImpl.processDataAvailable(InputBuffer.java:488)
    at org.apache.catalina.connector.InputBuffer$ReadHandlerImpl.onDataAvailable(InputBuffer.java:453)
    at org.glassfish.grizzly.http.io.InputBuffer.append(InputBuffer.java:855)
    at org.glassfish.grizzly.http.server.HttpServerFilter.handleRead(HttpServerFilter.java:222)
    at org.glassfish.grizzly.filterchain.ExecutorResolver$9.execute(ExecutorResolver.java:119)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeFilter(DefaultFilterChain.java:288)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.executeChainPart(DefaultFilterChain.java:206)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.execute(DefaultFilterChain.java:136)
    at org.glassfish.grizzly.filterchain.DefaultFilterChain.process(DefaultFilterChain.java:114)
    at org.glassfish.grizzly.ProcessorExecutor.execute(ProcessorExecutor.java:77)
    at org.glassfish.grizzly.nio.transport.TCPNIOTransport.fireIOEvent(TCPNIOTransport.java:838)
    at org.glassfish.grizzly.strategies.AbstractIOStrategy.fireIOEvent(AbstractIOStrategy.java:113)
    at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.run0(WorkerThreadIOStrategy.java:115)
    at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy.access$100(WorkerThreadIOStrategy.java:55)
    at org.glassfish.grizzly.strategies.WorkerThreadIOStrategy$WorkerThreadRunnable.run(WorkerThreadIOStrategy.java:135)
    at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.doWork(AbstractThreadPool.java:564)
    at org.glassfish.grizzly.threadpool.AbstractThreadPool$Worker.run(AbstractThreadPool.java:544)
    at java.lang.Thread.run(Thread.java:724)
Caused by: java.lang.ClassCastException: org.glassfish.json.JsonStringImpl cannot be cast to javax.json.JsonNumber
    at org.glassfish.json.JsonObjectBuilderImpl$JsonObjectImpl.getJsonNumber(JsonObjectBuilderImpl.java:171)
    at jp.co.epea.first.json.TestData.<init>(TestData.java:21)
    at jp.co.epea.first.json.TestDecoder.decode(TestDecoder.java:30)
    ... 27 more

WebSocketにてJSON形式のデータをやり取りしてみる(Glassfish 4)

Glassfish4とHTML間でJSON形式のデータをWebsocketにてやり取りしてみたのでメモ。

大まかな動きは以下

  1. HTMLからJSONで入力テキストと現在日時を送信。
  2. GlassfishのWebsocketで受信(&Decode)
  3. 受け取ったデータに情報(とりあえずユーザ情報代わりのセッションID)を付与し繋がっているクライアント(HTML)全体にJSON形式で返信
  4. HTMLで受け取ったデータを表示
(html->glassfishのデータ形式)
{"message":"メッセージ","messageTime":時間(ミリ秒)}

(glassfish->htmlのデータ形式)
{"message":"メッセージ","sessionId":"セッションID","messageTime":"HH:mm:ss"}

斜体は変数

Maven

必要な依存関係を追加。(動作確認はeclipse上のM2E)

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- 8/10追記 javaee-web-apiの中に入っていたもので足りた -->
        <!-- Websocket (JSR-356) -->
        <dependency>
            <groupId>javax.net.websocket</groupId>
            <artifactId>javax.net.websocket-api</artifactId>
            <version>1.0-b06</version>
            <type>jar</type>
        </dependency>
        <dependency>
            <groupId>org.glassfish.websocket</groupId>
            <artifactId>websocket-api</artifactId>
            <version>0.2</version>
            <scope>provided</scope>
        </dependency>
        <!-- JSON Processing (JSR-353) を使う場合-->
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.2 </version>
        </dependency>
    </dependencies>

画面

  1. new WebSocket(arg)のargで接続先指定をしてWebSocketオブジェクトを取得する
  2. 接続先は@ServerEndpointをつけたサーバサイドのPOJOに指定したもの。
  3. WebSocketのsendで送信、onmessageで受信
  4. Websocketで送られてきたデータはsocket.onmessageで渡される引数(今回はmessage)のdataに入っている。
  5. 取り出してからの扱いは普通のJSON。
 <!DOCTYPE html>
<html>
<head>
<title>JsonのWebSocketテスト</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script
    src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script type="text/javascript">
    var socket;
    $(document).ready(
            function() {
                var host = "ws://localhost:8080/first/hellojson/";
                socket = new WebSocket(host);

                socket.onmessage = function(message) {
                    try {
                        var wsRes = $.parseJSON(message.data);
                        $('#log').append(
                                wsRes.messageTime +
                                "(送信者=" + wsRes.sessionId + "):" + 
                                wsRes.message + "<br/>");
                    } catch (e) {
                        alert(e);
                        return;
                    }
                }

                $('#send').click(function() {
                    var obj = new Object();
                    obj.message = $('#msg').val();
                    obj.messageTime = new Date().getTime();
                    var jsonString = JSON.stringify(obj);
                    socket.send(jsonString);
                    $('#msg').val('');
                })

            });
</script>
</head>
<body>
    <h1>JsonのWebSocketテスト</h1>
    <div id="log"></div>
    <input id="msg" type="text" />
    <button id="send">送信</button>
</body>
</html>

WebSocket用のPOJO(GlassFish側)

  1. @ServerEndpointをつけたPOJOでWebSocket用クラス(サーバエンドポイント)を作る
  2. パスはvalueで指定。自分の環境ではコンテキストがfirstなのでfirst/hellojson/がサーバエンドポイントのアドレス
  3. @onMessageが繋がっている(クライアントサイドの)エンドポイントから呼ばれたときに動く個所
  4. decodersでクラスを指定すると、やってきたメッセージが指定されたクラスでデコードされて渡される。
  5. encodersでクラスを指定すると、指定したクラスでエンコードしたデータを、繋がっているエンドポイントに渡せる。
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

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

import jp.co.epea.first.json.TestData;
import jp.co.epea.first.json.TestDecoder;
import jp.co.epea.first.json.TestEncoder;

@ServerEndpoint(value = "/hellojson/", 
decoders = { TestDecoder.class }, encoders = { TestEncoder.class })
public class HelloJson {

    static Set<Session> sessions = Collections
            .synchronizedSet(new HashSet<Session>());

    @OnMessage
    public void onMessage(TestData data, Session sess) {
        System.out.println("Call HelloJson:data[" + data + "]");
        data.setSessionId(sess.getId());
        for (Session s : sessions) {
            s.getAsyncRemote().sendObject(data);
        }
    }

    @OnOpen
    public void open(Session sess) {
        System.out.println("開始します:" + sess.getId());
        sessions.add(sess);
    }

    @OnClose
    public void close(Session sess) {
        System.out.println("終了します:" + sess.getId());
        sessions.remove(sess);
    }

    @OnError
    public void error(Session sess, Throwable t) {
        System.out.println("エラーです:" + sess.getId());
        t.printStackTrace();
    }
}

decodersのクラス

  1. javax.websocket.DecoderのText<T>をimplementsする。(Text形式であるJSONの場合)
  2. <T>に入るのはエンドポイントのOnMessageに渡したい型
  3. willDecodeメソッドがデコードできるかどうか判定。
  4. バリデーションとかはwillDecodeでなくOnMessageでやるべきがどうかは未調査。(未実装)
  5. decodeメソッドで返却するオブジェクトを生成しreturnする。
  6. JSON Processingを使っているが何を使ってObject化してもOK
import java.io.StringReader;

import javax.json.Json;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.EndpointConfig;

public class TestDecoder implements Decoder.Text<TestData> {

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

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

@Override
public TestData decode(String inputString) throws DecodeException {
JsonObject jsonObject = Json.createReader(new StringReader(inputString)).readObject();
return  new TestData(jsonObject);
}

/*
* Answer whether the given String can be decoded into an object of type
* T.だそう 入力チェックもここでやるのがよい?
*/
@Override
public boolean willDecode(String inputString) {
try {
Json.createReader(new StringReader(inputString)).readObject();
return true;
} catch (JsonException ex) {
ex.printStackTrace();
return false;
}
}
}

encodersのクラス

  1. javax.websocket.EncoderのText<T>をimplementsする。(Text形式であるJSONの場合)
  2. <T>に入るのはエンドポイントのOnMessageで呼ぶsendObjectの引数(返す元ネタ)
  3. encodeメソッドで返却するStringを生成しreturnする。
  4. JSON Processingを使っているが何を使ってString化してもOK
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.json.Json;
import javax.json.JsonObject;
import javax.websocket.EncodeException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;

public class TestEncoder implements Encoder.Text {

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

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

	@Override
	public String encode(TestData paramData) throws EncodeException {
		JsonObject model = Json.createObjectBuilder()
				   .add("message", paramData.getMessage())
				   .add("sessionId",paramData.getSessionId())
				   .add("messageTime", formatDate(paramData.getMessageTime()))
				   .build();
		return model.toString();
	}

	private String formatDate(long millDate){
		return new SimpleDateFormat("HH:mm:ss").format(new Date(millDate));
	}

}

データクラス

import javax.json.JsonObject;

public class TestData {

    // メッセージ
    private String message;
    // 送信日時
    private long messageTime;
    // sessionId(ユーザ名の変わり)
    private String sessionId;

    public TestData(JsonObject jsonObject) {
        if(jsonObject.containsKey("message")){
            this.message = jsonObject.getString("message");
        }
        if(jsonObject.containsKey("messageTime")){
            this.messageTime = jsonObject.getJsonNumber("messageTime").longValue();
        }
    }

    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }

    String getMessage() {
        return message;
    }

    long getMessageTime() {
        return messageTime;
    }

    String getSessionId() {
        return sessionId;
    }
}

github[https://github.com/epea/test01/tree/WS_JSON]

WebSocketで@ServerEndpointが重複したときのエラー

@ServerEndpoint("/hello/")
public class HelloWorld {

と

@ServerEndpoint("/hello/")
public class HelloJson {

で起動時の

情報: Registering WebSocket filter for url pattern /*
といった次に
at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:131)
… 46 more
Caused by: javax.websocket.DeploymentException: Found Equivalent paths. Added path: ‘/first/hello/’ is equivalent with ‘/first/hello/’.

とわかりやすいメッセージがでる。

一応スタックトレース

情報: Registering WebSocket filter for url pattern /*
重大: WebModule[/first]Exception starting filter WebSocket filter
java.lang.InstantiationException
    at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:135)
    at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:5297)
    at org.apache.catalina.core.StandardContext.start(StandardContext.java:5909)
    at com.sun.enterprise.web.WebModule.start(WebModule.java:691)
    at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase.java:1041)
    at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:1024)
    at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:747)
    at com.sun.enterprise.web.WebContainer.loadWebModule(WebContainer.java:2278)
    at com.sun.enterprise.web.WebContainer.loadWebModule(WebContainer.java:1924)
    at com.sun.enterprise.web.WebApplication.start(WebApplication.java:139)
    at org.glassfish.internal.data.EngineRef.start(EngineRef.java:122)
    at org.glassfish.internal.data.ModuleInfo.start(ModuleInfo.java:291)
    at org.glassfish.internal.data.ApplicationInfo.start(ApplicationInfo.java:352)
    at com.sun.enterprise.v3.server.ApplicationLifecycle.deploy(ApplicationLifecycle.java:497)
    at com.sun.enterprise.v3.server.ApplicationLoaderService.processApplication(ApplicationLoaderService.java:407)
    at com.sun.enterprise.v3.server.ApplicationLoaderService.postConstruct(ApplicationLoaderService.java:243)
    at org.jvnet.hk2.internal.ClazzCreator.postConstructMe(ClazzCreator.java:281)
    at org.jvnet.hk2.internal.ClazzCreator.create(ClazzCreator.java:328)
    at org.jvnet.hk2.internal.SystemDescriptor.create(SystemDescriptor.java:448)
    at org.glassfish.hk2.runlevel.internal.AsyncRunLevelContext.findOrCreate(AsyncRunLevelContext.java:163)
    at org.jvnet.hk2.internal.Utilities.createService(Utilities.java:2204)
    at org.jvnet.hk2.internal.ServiceHandleImpl.getService(ServiceHandleImpl.java:93)
    at org.glassfish.hk2.runlevel.internal.CurrentTaskFuture$QueueRunner.oneJob(CurrentTaskFuture.java:673)
    at org.glassfish.hk2.runlevel.internal.CurrentTaskFuture$QueueRunner.run(CurrentTaskFuture.java:660)
    at org.glassfish.hk2.runlevel.internal.CurrentTaskFuture$UpOneJob.run(CurrentTaskFuture.java:490)
    at org.glassfish.hk2.runlevel.internal.CurrentTaskFuture$UpAllTheWay.go(CurrentTaskFuture.java:362)
    at org.glassfish.hk2.runlevel.internal.CurrentTaskFuture$UpAllTheWay.access$100(CurrentTaskFuture.java:279)
    at org.glassfish.hk2.runlevel.internal.CurrentTaskFuture.go(CurrentTaskFuture.java:113)
    at org.glassfish.hk2.runlevel.internal.AsyncRunLevelContext.proceedTo(AsyncRunLevelContext.java:296)
    at org.glassfish.hk2.runlevel.internal.RunLevelControllerImpl.proceedTo(RunLevelControllerImpl.java:66)
    at com.sun.enterprise.v3.server.AppServerStartup.proceedTo(AppServerStartup.java:532)
    at com.sun.enterprise.v3.server.AppServerStartup.run(AppServerStartup.java:329)
    at com.sun.enterprise.v3.server.AppServerStartup.doStart(AppServerStartup.java:226)
    at com.sun.enterprise.v3.server.AppServerStartup.start(AppServerStartup.java:217)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishImpl.start(GlassFishImpl.java:79)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishDecorator.start(GlassFishDecorator.java:63)
    at com.sun.enterprise.glassfish.bootstrap.osgi.EmbeddedOSGiGlassFishImpl.start(EmbeddedOSGiGlassFishImpl.java:75)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishDecorator.start(GlassFishDecorator.java:63)
    at com.sun.enterprise.glassfish.bootstrap.osgi.OSGiGlassFishImpl.start(OSGiGlassFishImpl.java:71)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishMain$Launcher.launch(GlassFishMain.java:117)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at com.sun.enterprise.glassfish.bootstrap.GlassFishMain.main(GlassFishMain.java:97)
    at com.sun.enterprise.glassfish.bootstrap.ASMain.main(ASMain.java:54)
Caused by: javax.servlet.ServletException: Web socket server initialization failed.
    at org.glassfish.tyrus.servlet.TyrusServletFilter.init(TyrusServletFilter.java:135)
    at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:275)
    at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:131)
    ... 45 more
Caused by: javax.websocket.DeploymentException: Found Equivalent paths. Added path: '/first/hello/' is equivalent with '/first/hello/'.
    at org.glassfish.tyrus.core.ErrorCollector.composeComprehensiveException(ErrorCollector.java:83)
    at org.glassfish.tyrus.server.TyrusServerContainer.start(TyrusServerContainer.java:144)
    at org.glassfish.tyrus.servlet.TyrusServletFilter.init(TyrusServletFilter.java:133)
    ... 47 more

情報: Loading application [first] at [/first]
情報: Loading application first done in 920 ms
情報: GlassFish Server Open Source Edition  4.0  (89) startup time : Felix (2,834ms), startup services(5,306ms), total(8,140ms)
情報: Initiating Jersey application, version Jersey: 2.0 2013-05-03 14:50:15...
情報: JMXStartupService has started JMXConnector on JMXService URL service:jmx:rmi://epeapc01-PC:8686/jndi/rmi://epeapc01-PC:8686/jmxrmi
情報: Grizzly Framework 2.3.1 started in: 10ms - bound to [/0.0.0.0:7,676]
情報: Registered com.sun.enterprise.glassfish.bootstrap.osgi.EmbeddedOSGiGlassFishImpl@30c2555a as OSGi service registration: org.apache.felix.framework.ServiceRegistrationImpl@19bd20.

JSON Processing(JSR-353)に寄り道1

Websocketで複数のデータを送ろうとした際に、JavaEE7でJSON Processingが出てたのを思い出したので触ってみた。

ArunさんのブログにあるようにJSONとObject間のデータバインディングまではやってくれない。

(Note, binding JSON to Java objects and vice versa is not part of the scope of this JSR.)

想定用途(?)を探して見つけたブログによるとStaxでDom操作する感覚で使えるよとのこと。

->JCPにしっかり書いてあった。(2.1)

 

利用設定(Maven)

pomに依存関係を追加

        <!-- JSON Processing (JSR-353) -->
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.2 </version>
        </dependency>

試したソース

import javax.json.Json;
import javax.json.JsonObject;
import javax.json.stream.JsonParser;

public class FirstJson01 {
    public static void main(String[] args) {
        System.out.println("生成");
        JsonObject model = Json.createObjectBuilder()
                   .add("firstName", "Duke")
                   .add("lastName", "Java")
                   .add("age", 18)
                   .add("streetAddress", "100 Internet Dr")
                   .add("city", "JavaTown")
                   .add("state", "JA")
                   .add("postalCode", "12345")
                   .add("phoneNumbers", Json.createArrayBuilder()
                      .add(Json.createObjectBuilder()
                         .add("type", "mobile")
                         .add("number", "111-111-1111"))
                      .add(Json.createObjectBuilder()
                         .add("type", "home")
                         .add("number", "222-222-2222")))
                   .build();

        String text = model.toString();
        System.out.println(text);

        System.out.println("解析");
        StringReader sr = new StringReader(text);
        JsonParser parser = Json.createParser(sr);
        while(parser.hasNext()){
            JsonParser.Event event = parser.next();
            System.out.println(event);
        }
    }
}

Eventの種類はSTART_OBJECT/END_OBJECT、START_ARRAY/END_ARRAY、KEY_NAME、VALUE_STRING、VALUE_NUMBER、VALUE_TRUE、VALUE_FALSE、VALUE_NULL。

 

 

 

JavaFXとWebsocketを連携してみる(On Glassfish4.0) その1

サーバサイドはいつものようにGlassfish 4.0。まずは寺田さんのブログを見つつメッセージ送信用のページで入れたメッセージが表示されるようにするとこまで持っていく。(元のブログのようにツイッターを見に行きはしないで、index.htmlで入れたものが表示されるようにする。)

サーバ用のエンドポイント(ws://localhost:8080/first/hello/)

package jp.co.epea.first;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

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

@ServerEndpoint("/hello/")
public class HelloWorld {

    static Set<Session> sessions = Collections
            .synchronizedSet(new HashSet<Session>());

    @OnMessage
    public void onMessage(String message) {
        System.out.println("message[" + message + "]");

        for (Session s : sessions) {
            s.getAsyncRemote().sendText(message);
        }
    }

    @OnOpen
    public void open(Session sess) {
        System.out.println("開始します:" + sess.getId());
        sessions.add(sess);
    }

    @OnClose
    public void close(Session sess) {
        System.out.println("終了します:" + sess.getId());
        sessions.remove(sess);
    }

    @OnError
    public void error(Session sess,Throwable t) {
        System.out.println("エラーです:" + sess.getId());
        t.printStackTrace();
    }
}

メッセージ送信用のページ(http://localhost:8080/first/index.html)

<!DOCTYPE html>
<html>
    <head>
        <title>WebSocketテスト</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
        <script type="text/javascript">
            var socket;
            $(document).ready(function(){
                var host="ws://localhost:8080/first/hello/";
                socket = new WebSocket(host);

                socket.onmessage = function(message){
                    $('#log').append(message.data + "<br/>");
                }

                $('#send').click(function(){
                    var text = $('#msg').val();
                    socket.send(text);
                    $('#msg').val('');
                })

            });
        </script>        
    </head>
    <body>
        <h1>WebSocketテスト</h1>
        <div id="log">
        </div>
        <input id="msg" type="text"/>
        <button id="send">送信</button>
    </body>
</html>

 

クライアントサイドの作成の流れは

  1. MavanプロジェクトとしてJavaFXを作成(前回ブログ)
  2. 足りない依存関係を追加
  3. 寺田さんのブログのクライアントサイドプログラムをコピペ
  4. とりあえず動くようになるために最低限の修正を加える

(手順1と3は省略)

2.足りない依存関係を追加

tyrus-clientとtyrus-container-grizzlyはこちらのブログを参考にしたときに「Grizzly 関連の jar」の今版として使えそうなので試したところいけたっぽいので持ってきた。(ユーザガイド)

    <dependencies>
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>javafx</artifactId>
            <version>2.0</version>
            <scope>system</scope>
            <systemPath>C:\dev\pleiades43\java\7\lib/jfxrt.jar</systemPath>
        </dependency>
        <dependency>
            <groupId>org.glassfish.tyrus</groupId>
            <artifactId>tyrus-client</artifactId>
            <version>1.1</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.tyrus</groupId>
            <artifactId>tyrus-container-grizzly</artifactId>
            <version>1.1</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

 

4.とりあえず動くようになるために最低限の修正を加える

TwitterClientのアノテーションを@ClientEndpoint、@OnOpen、@OnMessage、@OnCloseに変更。

SampleController.javaのURLを修正。

ClientEndpointConfigurationの行をコメントアウト。

URI clientURI = new URI("ws://localhost:8080/first/hello/");
//ClientEndpointConfiguration clientConfig = new DefaultClientConfiguration();

 

動作確認

サーバを起動する。

JavaFXを起動する。

a

start timelineボタンをクリックしてサーバと接続

(コンソールメッセージ)
Connection had opened.

 

index.htmlからメッセージを送信

b

 

JavaFX側に表示される

c

 

一応これで動作しているようなのでこいつを基点に触っていくことにする。

(JavaFX側をXボタンで閉じたときにOncloseが呼ばれてないっぽいけど、実装的にそういう動きになるのか環境がおかしいのか。。。。。)

2013/11/03追記

子の実装ではJavaFXのTableViewを使用することで、WebSocket側のスレッドによる変更がJavaFXのスレッドに通知されている。(オブザーバ)

桜庭さんに伺ったところ、「(GUIはそっちのスレッドから動かすのが基本なので)きちんと動かないことあるかもよ」とのことでした。

他の日記ではLabelで同期しようとした場合にPlatform.runLaterでJavaFXのスレッドに処理を戻しているけど、そちらの方法が基本とのことでした。