001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.hayabusa.servlet;
017
018import jakarta.websocket.OnOpen;
019import jakarta.websocket.OnClose;
020import jakarta.websocket.OnMessage;
021import jakarta.websocket.OnError;
022import jakarta.websocket.Session;
023import jakarta.websocket.EndpointConfig;
024import jakarta.websocket.CloseReason;
025import jakarta.websocket.server.ServerEndpoint;
026import java.io.IOException;
027import java.io.ByteArrayInputStream;
028import java.io.ByteArrayOutputStream;
029import java.nio.ByteBuffer;
030import java.awt.image.BufferedImage;
031import javax.imageio.ImageIO;
032import java.util.Set;
033import java.util.Collections;
034import java.util.concurrent.ConcurrentHashMap;
035
036/**
037 * http://enterprisegeeks.hatenablog.com/entry/2015/12/17/104815
038 *
039 * WebSocketBasicEndpoint.java のソースを参照しています。
040 *
041 * 設定が、いくつか必要です。
042 *   ① /wsdemo をサーバーエンドポイントのURLにしているため
043 *      WEB-INF/web.xml の security-constraint の web-resource-collection の
044 *      url-pattern に、/wsdemo を追加する必要がある。
045 *        <url-pattern>/wsdemo/*</url-pattern>
046 *
047 *   ② コンパイル時(build.xml)のクラスパスの設定に、
048 *      <pathelement path="${env.CATALINA_HOME}/lib/websocket-api.jar" />
049 *      を追加する必要がある。
050 */
051@ServerEndpoint(value="/wsdemo")
052public class WebSocketDemo {
053
054        /** クライアントからの全接続を保持するセット */
055        /**
056         * クライアントからの全接続を保持するセット
057         *
058         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
059         */
060        private static final Set<Session> SESS_SET = Collections.newSetFromMap( new ConcurrentHashMap<Session, Boolean>() );
061
062        /**
063         * デフォルトコンストラクター
064         *
065         */
066        public WebSocketDemo() { super(); }             // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
067
068        /**
069         * 2.クライアントからの接続時にコールされる。
070         *
071         * 引数は以下が設定可能だが、メソッド内で使用しないなら省略できる。
072         *
073         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
074         *
075         * @param client クライアントの接続情報
076         * @param config 設定情報
077         */
078        @OnOpen
079        public void onOpen( final Session client, final EndpointConfig config ) {
080                System.out.println( client.getId() + " was connected." );
081                printSession( client );
082
083                SESS_SET.add( client );
084        }
085
086        /**
087         * 3.クライアントの切断時にコールされる
088         *
089         * 引数は使用しなければ省略可能。
090         *
091         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
092         *
093         * @param client 接続
094         * @param reason 切断理由
095         */
096        @OnClose
097        public void onClose( final Session client, final CloseReason reason ) {
098                System.out.println( client.getId() + " was closed by "
099                                                                +               reason.getCloseCode()
100                                                                + "[" + reason.getCloseCode().getCode()+"]" );
101                printSession( client );
102
103                SESS_SET.remove( client );
104        }
105
106        /**
107         * 4.エラー時にコールされる。
108         *
109         * 引数は使用しなければ省略可能。
110         * @param client クライアント接続
111         * @param error エラー
112         */
113        @OnError
114        public void onError( final Session client, final Throwable error ) {
115                System.out.println( client.getId() + " was error." );
116                error.printStackTrace();
117        }
118
119        /**
120         * 5.テキストメッセージ受信時の処理
121         *
122         * 全クライアントにメッセージを送信する。(サンプル)
123         *
124         * 引数は使用しなければ省略可能。
125         * @param text クライアントから送信されたテキスト
126         * @param client 接続情報
127         * @throws IOException  なんらかの入出力例外の発生を通知するシグナルを発生させます。
128         */
129        @OnMessage
130        public void onMessage( final String text, final Session client ) throws IOException {
131                System.out.println( client.getId() + " was message." + text );
132
133        //      // sessionが持つendPointに紐付く session を取り出す。
134        //      for( final Session other : client.getOpenSessions() ) {
135        //              other.getBasicRemote().sendText( text );
136        //      }
137                broadCast( text );              // Setで管理しているopenされたsession (元とかぶる)
138        }
139
140        /**
141         * すべてのクライアントにメッセージを送信する。
142         *
143         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
144         *
145         * @param text クライアントに送信するテキスト
146         */
147        private static void broadCast( final String text ) {
148                for( final Session client : SESS_SET) {
149                        client.getOpenSessions().forEach( sess -> sess.getAsyncRemote().sendText( text ) );
150                }
151        }
152
153        /**
154         * 6.バイナリ受信時の処理
155         *
156         * 送信元に画像を変換して送り返す。
157         *
158         * 引数は使用しなければ省略可能。
159         * @param buf クライアントから送信されたバイナリ
160         * @param client 接続情報
161         * @throws IOException  なんらかの入出力例外の発生を通知するシグナルを発生させます。
162         */
163        @OnMessage
164        public void onMessage( final ByteBuffer buf, final Session client ) throws IOException {
165                client.getBasicRemote().sendBinary( grayScall( buf ) );
166        }
167
168        /**
169         * 7.画像をグレースケールに変換する。本筋とは関係ない。
170         *
171         * @param input 引数のバイトバッファー
172         * @return グレースケールに変換されたバイトバッファー
173         */
174        private ByteBuffer grayScall( final ByteBuffer input ) throws IOException {
175                final BufferedImage img = ImageIO.read( new ByteArrayInputStream( input.array() ) );
176                final BufferedImage glay = new BufferedImage( img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY );
177                glay.getGraphics().drawImage( img, 0, 0, null );
178                final ByteArrayOutputStream bos = new ByteArrayOutputStream();
179                ImageIO.write( glay, "png", bos );
180
181                return ByteBuffer.wrap( bos.toByteArray() );
182        }
183
184        /**
185         * Session のセッションを標準出力に表示します。
186         *
187         * デバッグ用の仮メソッドです。
188         *
189         * @param session セッションオブジェクト
190         */
191        private void printSession( final Session session ) {
192                System.out.println( " ID    = " + session.getId() );
193        //      System.out.println( " Query = " + session.getQueryString() );           // 引数部分のみ
194                System.out.println( " URI   = " + session.getRequestURI() );            // URI すべて
195
196                System.out.println( " RequestParameter : " );                                           // 引数部分を、展開して、Map化
197                session.getRequestParameterMap().forEach( (k,v) -> {
198                                System.out.println( "  Key=" + k );
199                                System.out.println( "  Val=" + java.util.Arrays.toString( v.toArray( new String[v.size()] ) ) );
200                        }
201                );
202        }
203}