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("/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        // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseDiamondOperator 対応
061//      private static final Set<Session> SESS_SET = Collections.newSetFromMap( new ConcurrentHashMap<Session, Boolean>() );
062        private static final Set<Session> SESS_SET = Collections.newSetFromMap( new ConcurrentHashMap<>() );
063
064        /**
065         * デフォルトコンストラクター
066         *
067         */
068        public WebSocketDemo() { super(); }             // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
069
070        /**
071         * 2.クライアントからの接続時にコールされる。
072         *
073         * 引数は以下が設定可能だが、メソッド内で使用しないなら省略できる。
074         *
075         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
076         *
077         * @param client クライアントの接続情報
078         * @param config 設定情報
079         */
080        @OnOpen
081        public void onOpen( final Session client, final EndpointConfig config ) {
082                System.out.println( client.getId() + " was connected." );
083                printSession( client );
084
085                SESS_SET.add( client );
086        }
087
088        /**
089         * 3.クライアントの切断時にコールされる
090         *
091         * 引数は使用しなければ省略可能。
092         *
093         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
094         *
095         * @param client 接続
096         * @param reason 切断理由
097         */
098        @OnClose
099        public void onClose( final Session client, final CloseReason reason ) {
100                System.out.println( client.getId() + " was closed by "
101                                                                +               reason.getCloseCode()
102                                                                + "[" + reason.getCloseCode().getCode()+"]" );
103                printSession( client );
104
105                SESS_SET.remove( client );
106        }
107
108        /**
109         * 4.エラー時にコールされる。
110         *
111         * 引数は使用しなければ省略可能。
112         * @param client クライアント接続
113         * @param error エラー
114         */
115        @OnError
116        public void onError( final Session client, final Throwable error ) {
117                System.out.println( client.getId() + " was error." );
118                error.printStackTrace();
119        }
120
121        /**
122         * 5.テキストメッセージ受信時の処理
123         *
124         * 全クライアントにメッセージを送信する。(サンプル)
125         *
126         * 引数は使用しなければ省略可能。
127         * @param text クライアントから送信されたテキスト
128         * @param client 接続情報
129         * @throws IOException  なんらかの入出力例外の発生を通知するシグナルを発生させます。
130         */
131        @OnMessage
132        public void onMessage( final String text, final Session client ) throws IOException {
133                System.out.println( client.getId() + " was message." + text );
134
135        //      // sessionが持つendPointに紐付く session を取り出す。
136        //      for( final Session other : client.getOpenSessions() ) {
137        //              other.getBasicRemote().sendText( text );
138        //      }
139                broadCast( text );              // Setで管理しているopenされたsession (元とかぶる)
140        }
141
142        /**
143         * すべてのクライアントにメッセージを送信する。
144         *
145         * @og.rev 6.8.5.0 (2018/01/09) PMD Variables that are final and static should be all capitals。sessSET → SESS_SET
146         *
147         * @param text クライアントに送信するテキスト
148         */
149        private static void broadCast( final String text ) {
150                for( final Session client : SESS_SET) {
151                        client.getOpenSessions().forEach( sess -> sess.getAsyncRemote().sendText( text ) );
152                }
153        }
154
155        /**
156         * 6.バイナリ受信時の処理
157         *
158         * 送信元に画像を変換して送り返す。
159         *
160         * 引数は使用しなければ省略可能。
161         * @param buf クライアントから送信されたバイナリ
162         * @param client 接続情報
163         * @throws IOException  なんらかの入出力例外の発生を通知するシグナルを発生させます。
164         */
165        @OnMessage
166        public void onMessage( final ByteBuffer buf, final Session client ) throws IOException {
167                client.getBasicRemote().sendBinary( grayScall( buf ) );
168        }
169
170        /**
171         * 7.画像をグレースケールに変換する。本筋とは関係ない。
172         *
173         * @param input 引数のバイトバッファー
174         * @return グレースケールに変換されたバイトバッファー
175         * @throws IOException 入出力エラー
176         */
177        private ByteBuffer grayScall( final ByteBuffer input ) throws IOException {
178                final BufferedImage img = ImageIO.read( new ByteArrayInputStream( input.array() ) );
179                final BufferedImage glay = new BufferedImage( img.getWidth(), img.getHeight(), BufferedImage.TYPE_BYTE_GRAY );
180                glay.getGraphics().drawImage( img, 0, 0, null );
181                final ByteArrayOutputStream bos = new ByteArrayOutputStream();
182                ImageIO.write( glay, "png", bos );
183
184                return ByteBuffer.wrap( bos.toByteArray() );
185        }
186
187        /**
188         * Session のセッションを標準出力に表示します。
189         *
190         * デバッグ用の仮メソッドです。
191         *
192         * @param session セッションオブジェクト
193         */
194        private void printSession( final Session session ) {
195                System.out.println( " ID    = " + session.getId() );
196        //      System.out.println( " Query = " + session.getQueryString() );           // 引数部分のみ
197                System.out.println( " URI   = " + session.getRequestURI() );            // URI すべて
198
199                System.out.println( " RequestParameter : " );                                           // 引数部分を、展開して、Map化
200                session.getRequestParameterMap().forEach( (k,v) -> {
201                                System.out.println( "  Key=" + k );
202                                System.out.println( "  Val=" + java.util.Arrays.toString( v.toArray( new String[v.size()] ) ) );
203                        }
204                );
205        }
206}