├── Dockerfile ├── LICENSE ├── README.md ├── pom.xml ├── screenshot ├── desktop.png ├── login.png └── phone.png └── src └── main ├── java └── com │ └── yusufsezer │ ├── ChatEndpoint.java │ ├── Message.java │ ├── MessageDecoder.java │ └── MessageEncoder.java └── webapp ├── META-INF └── context.xml ├── WEB-INF ├── beans.xml └── web.xml ├── index.html └── script.js /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Maven Build 2 | FROM maven AS build 3 | WORKDIR /usr/src/app 4 | COPY . . 5 | RUN mvn clean package 6 | 7 | # Stage 2: Tomcat Runtime 8 | FROM tomcat 9 | RUN ls . 10 | COPY --from=build /usr/src/app/server/target/java-websocket-example.war /usr/local/tomcat/webapps/ROOT.war 11 | EXPOSE 8080 12 | 13 | CMD ["catalina.sh", "run"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yusuf Sezer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java WebSocket Example 2 | A simple Java WebSocket chat application developed with Java, Java JSONB. 3 | 4 | ## [Download](https://github.com/yusufsefasezer/java-websocket-example/archive/master.zip) 5 | 6 | ## How to run 7 | 8 | Maven must be installed to run this application. 9 | 10 | If you have maven execute the below command to run. 11 | 12 | ``` 13 | mvn clean package cargo:run 14 | ``` 15 | 16 | You can access the application using `localhost:8080/java-websocket-example` in your web browser. 17 | 18 | ## Docker 19 | 20 | **Docker must be installed.** 21 | 22 | Build the Docker image with the tag "java-websocket-example" 23 | 24 | ``` 25 | docker build -t java-websocket-example . 26 | ``` 27 | 28 | ``` 29 | docker run -p 80:8080 java-websocket-example 30 | ``` 31 | 32 | You can access the application using `localhost:80` in your web browser. 33 | 34 | ## Screenshot 35 | 36 | - [Login](screenshot/login.png) 37 | - [Desktop](screenshot/desktop.png) 38 | - [Phone](screenshot/phone.png) 39 | 40 | # License 41 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details 42 | 43 | Created by [Yusuf Sezer](https://www.yusufsezer.com) 44 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 4.0.0 5 | com.yusufsezer 6 | java-websocket-example 7 | 1.1 8 | war 9 | 10 | 11 | UTF-8 12 | 21 13 | 21 14 | 15 | 16 | 17 | 18 | 19 | jakarta.websocket 20 | jakarta.websocket-api 21 | 2.2.0-M1 22 | provided 23 | 24 | 25 | 26 | jakarta.websocket 27 | jakarta.websocket-client-api 28 | 2.2.0-M1 29 | provided 30 | 31 | 32 | 33 | org.eclipse 34 | yasson 35 | 3.0.3 36 | 37 | 38 | 39 | 40 | ${project.artifactId} 41 | 42 | 43 | 44 | org.codehaus.cargo 45 | cargo-maven3-plugin 46 | 1.10.12 47 | 48 | tomcat11x 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /screenshot/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yusufsefasezer/java-websocket-example/e5a4d888fb63c95762dcb81737036fae2ef5d518/screenshot/desktop.png -------------------------------------------------------------------------------- /screenshot/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yusufsefasezer/java-websocket-example/e5a4d888fb63c95762dcb81737036fae2ef5d518/screenshot/login.png -------------------------------------------------------------------------------- /screenshot/phone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yusufsefasezer/java-websocket-example/e5a4d888fb63c95762dcb81737036fae2ef5d518/screenshot/phone.png -------------------------------------------------------------------------------- /src/main/java/com/yusufsezer/ChatEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.yusufsezer; 2 | 3 | import jakarta.websocket.CloseReason; 4 | import jakarta.websocket.EncodeException; 5 | import jakarta.websocket.OnClose; 6 | import jakarta.websocket.OnError; 7 | import jakarta.websocket.OnMessage; 8 | import jakarta.websocket.Session; 9 | import jakarta.websocket.server.ServerEndpoint; 10 | import java.io.IOException; 11 | 12 | @ServerEndpoint( 13 | value = "/chat", 14 | decoders = MessageDecoder.class, 15 | encoders = MessageEncoder.class 16 | ) 17 | public class ChatEndpoint { 18 | 19 | @OnMessage 20 | public void onMessage(Session session, Message message) { 21 | if ("setUserName".equals(message.getAction())) { 22 | String username = message.getValue(); 23 | setUserName(session, username); 24 | } else if ("sendMessage".equals(message.getAction())) { 25 | String msg = message.getValue(); 26 | sendMessage(session, msg); 27 | } 28 | } 29 | 30 | @OnError 31 | public void onError(Session session, Throwable error) { 32 | closeSession(session); 33 | } 34 | 35 | @OnClose 36 | public void onClose(Session session, CloseReason closeReason) { 37 | String username = getUserName(session); 38 | String size = String.valueOf(getUserSize(session)); 39 | Message leftMessage = new Message("left", username, size); 40 | sendAllMessage(session, leftMessage); 41 | } 42 | 43 | private int getUserSize(Session session) { 44 | return session 45 | .getOpenSessions() 46 | .size(); 47 | } 48 | 49 | private void setUserName(Session session, String username) { 50 | boolean isTaken = isTaken(session, username); 51 | if (isTaken) { 52 | closeSession(session); 53 | return; 54 | } 55 | session.getUserProperties().put("username", username); 56 | String size = String.valueOf(getUserSize(session)); 57 | Message joinMessage = new Message("join", username, size); 58 | sendAllMessage(session, joinMessage); 59 | } 60 | 61 | private void sendMessage(Session session, String message) { 62 | String username = getUserName(session); 63 | Message newMessage = new Message("newMessage", username, message); 64 | sendAllMessage(session, newMessage); 65 | } 66 | 67 | private String getUserName(Session session) { 68 | return String.valueOf(session.getUserProperties().get("username")); 69 | } 70 | 71 | private boolean isTaken(Session session, String username) { 72 | return session 73 | .getOpenSessions() 74 | .stream() 75 | .anyMatch(p -> p.getUserProperties().containsValue(username)); 76 | } 77 | 78 | private void sendAllMessage(Session session, Message message) { 79 | session 80 | .getOpenSessions() 81 | .stream() 82 | .filter(p -> p.isOpen() == true) 83 | .forEach((s) -> { 84 | try { 85 | s.getBasicRemote().sendObject(message); 86 | } catch (IOException | EncodeException e) { 87 | System.err.println(e); 88 | } 89 | }); 90 | } 91 | 92 | private void closeSession(Session session) { 93 | try { 94 | session.close(); 95 | } catch (IOException ex) { 96 | System.err.println(ex); 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/yusufsezer/Message.java: -------------------------------------------------------------------------------- 1 | package com.yusufsezer; 2 | 3 | import java.time.LocalDateTime; 4 | 5 | public class Message { 6 | 7 | private String action; 8 | private String value; 9 | private String username; 10 | private LocalDateTime date; 11 | 12 | public Message() { 13 | this.date = LocalDateTime.now(); 14 | } 15 | 16 | public Message(String action, String username, String value) { 17 | this.action = action; 18 | this.username = username; 19 | this.value = value; 20 | this.date = LocalDateTime.now(); 21 | } 22 | 23 | public Message(String action, String value) { 24 | this.action = action; 25 | this.value = value; 26 | this.date = LocalDateTime.now(); 27 | } 28 | 29 | public String getAction() { 30 | return action; 31 | } 32 | 33 | public void setAction(String action) { 34 | this.action = action; 35 | } 36 | 37 | public String getValue() { 38 | return value; 39 | } 40 | 41 | public void setValue(String value) { 42 | this.value = value; 43 | } 44 | 45 | public String getUsername() { 46 | return username; 47 | } 48 | 49 | public void setUsername(String username) { 50 | this.username = username; 51 | } 52 | 53 | public LocalDateTime getDate() { 54 | return date; 55 | } 56 | 57 | public void setDate(LocalDateTime date) { 58 | this.date = date; 59 | } 60 | 61 | @Override 62 | public String toString() { 63 | return "Message{" 64 | + "action=" + action 65 | + ", value=" + value 66 | + ", username=" + username 67 | + ", date=" + date + '}'; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/yusufsezer/MessageDecoder.java: -------------------------------------------------------------------------------- 1 | package com.yusufsezer; 2 | 3 | import jakarta.json.bind.JsonbBuilder; 4 | import jakarta.json.bind.JsonbException; 5 | import jakarta.websocket.DecodeException; 6 | import jakarta.websocket.Decoder; 7 | import jakarta.websocket.EndpointConfig; 8 | 9 | public class MessageDecoder implements Decoder.Text { 10 | 11 | @Override 12 | public Message decode(String text) throws DecodeException { 13 | try { 14 | return JsonbBuilder.create().fromJson(text, Message.class); 15 | } catch (JsonbException e) { 16 | throw new DecodeException(text, e.getMessage(), e); 17 | } 18 | } 19 | 20 | @Override 21 | public boolean willDecode(String text) { 22 | return true; 23 | } 24 | 25 | @Override 26 | public void init(EndpointConfig ec) { 27 | } 28 | 29 | @Override 30 | public void destroy() { 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/yusufsezer/MessageEncoder.java: -------------------------------------------------------------------------------- 1 | package com.yusufsezer; 2 | 3 | import jakarta.json.bind.JsonbBuilder; 4 | import jakarta.websocket.EncodeException; 5 | import jakarta.websocket.Encoder; 6 | import jakarta.websocket.EndpointConfig; 7 | 8 | public class MessageEncoder implements Encoder.Text { 9 | 10 | @Override 11 | public String encode(Message message) throws EncodeException { 12 | return JsonbBuilder.create().toJson(message); 13 | } 14 | 15 | @Override 16 | public void init(EndpointConfig ec) { 17 | } 18 | 19 | @Override 20 | public void destroy() { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/webapp/META-INF/context.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/beans.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 30 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Java WebSocket Example 7 | 8 | 9 | 10 | 11 | 12 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Java WebSocket Example 34 | 4 user online 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Login 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | SEND 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/webapp/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var URI = 'chat'; 4 | 5 | var buttonLogin = null, 6 | buttonSend = null, 7 | inputUsername = null, 8 | spanStatus = null, 9 | chatForm = null, 10 | chatBody = null, 11 | chatFooter = null, 12 | ws = null, 13 | onlineStatus = null, 14 | messageList = null, 15 | textareaMessage = null; 16 | 17 | function init() { 18 | initDOM(); 19 | initEvent(); 20 | } 21 | 22 | function initDOM() { 23 | buttonLogin = document.getElementById('buttonLogin'); 24 | buttonSend = document.getElementById('buttonSend'); 25 | inputUsername = document.getElementById('inputUsername'); 26 | spanStatus = document.getElementById('spanStatus'); 27 | chatForm = document.getElementById('chatForm'); 28 | chatBody = document.getElementById('chatBody'); 29 | chatFooter = document.getElementById('chatFooter'); 30 | onlineStatus = document.getElementById('onlineStatus'); 31 | messageList = document.getElementById('messageList'); 32 | textareaMessage = document.getElementById('textareaMessage'); 33 | } 34 | 35 | function initEvent() { 36 | inputUsername.addEventListener('keypress', checkLogin); 37 | buttonLogin.addEventListener('click', connectServer); 38 | textareaMessage.addEventListener('keypress', checkSend); 39 | buttonSend.addEventListener('click', sendNewMessage); 40 | } 41 | 42 | function checkLogin(e) { 43 | if (e.key == 'Enter') { 44 | buttonLogin.click(); 45 | } 46 | } 47 | 48 | function checkSend(e) { 49 | if (e.key == 'Enter') { 50 | buttonSend.click(); 51 | e.preventDefault(); 52 | } 53 | } 54 | 55 | function connectServer() { 56 | var isEmpty = checkValue(inputUsername.value); 57 | 58 | if (isEmpty) { 59 | setStatus('Please enter username'); 60 | return; 61 | } 62 | 63 | try { 64 | ws = new WebSocket(createURI(URI)); 65 | ws.addEventListener('open', onOpen); 66 | ws.addEventListener('message', onMessage); 67 | ws.addEventListener('error', onError); 68 | ws.addEventListener('close', onClose); 69 | } catch (e) { 70 | setStatus(e.message); 71 | } 72 | } 73 | 74 | function createURI(URI) { 75 | var newURI = "ws://"; 76 | var location = window.location; 77 | if (location.protocol === 'https:') { 78 | newURI = "wss://" 79 | } 80 | return newURI + location.host + location.pathname + URI; 81 | } 82 | 83 | function onOpen(e) { 84 | initCustomEvent(); 85 | sendMessage('setUserName', inputUsername.value); 86 | setConnect(); 87 | setStatus('Connected succesfully.'); 88 | textareaMessage.focus(); 89 | } 90 | 91 | function initCustomEvent() { 92 | ws.addEventListener('join', onJoin); 93 | ws.addEventListener('left', onLeft); 94 | ws.addEventListener('newMessage', onNewMessage); 95 | } 96 | 97 | function onMessage(e) { 98 | var message = JSON.parse(e.data); 99 | 100 | ws.dispatchEvent(new CustomEvent(message.action, { 101 | detail: message 102 | })); 103 | } 104 | 105 | function onError(e) { 106 | setDisconnect(); 107 | setStatus('Something went wrong: ' + e); 108 | } 109 | 110 | function onClose(e) { 111 | setDisconnect(); 112 | setStatus(e.reason); 113 | } 114 | 115 | function setStatus(message) { 116 | spanStatus.textContent = message; 117 | setTimeout(function () { 118 | spanStatus.textContent = null; 119 | }, 1500); 120 | } 121 | 122 | function sendMessage(action, value) { 123 | ws.send(JSON.stringify({ 124 | action: action, 125 | value: value 126 | })); 127 | } 128 | 129 | function addMessage(content) { 130 | var messageElement = document.createElement('li'); 131 | messageElement.textContent = content; 132 | messageElement.className = 'text-center'; 133 | messageList.insertAdjacentElement('beforeend', messageElement); 134 | } 135 | 136 | function setOnlineCount(count) { 137 | onlineStatus.textContent = count + ' user online'; 138 | } 139 | 140 | function onJoin(e) { 141 | addMessage(e.detail.username + ' joined.'); 142 | setOnlineCount(e.detail.value); 143 | chatBody.scrollTop = chatBody.scrollHeight; 144 | } 145 | 146 | function onLeft(e) { 147 | addMessage(e.detail.username + ' left.'); 148 | setOnlineCount(e.detail.value); 149 | chatBody.scrollTop = chatBody.scrollHeight; 150 | } 151 | 152 | function onNewMessage(e) { 153 | var messageDate = new Date(e.detail.date); 154 | var messageElement = document.createElement('li'); 155 | messageElement.textContent = e.detail.value; 156 | var messageInfo = document.createElement('strong'); 157 | messageInfo.textContent = '(' + messageDate.toLocaleTimeString() + ') ' + e.detail.username + ': '; 158 | messageElement.insertAdjacentElement('afterbegin', messageInfo); 159 | messageList.insertAdjacentElement('beforeend', messageElement); 160 | chatBody.scrollTop = chatBody.scrollHeight; 161 | } 162 | 163 | function setConnect() { 164 | onlineStatus.classList.remove('d-none'); 165 | chatForm.classList.add('d-none'); 166 | chatBody.classList.remove('d-none'); 167 | chatFooter.classList.remove('d-none'); 168 | } 169 | 170 | function setDisconnect() { 171 | onlineStatus.classList.add('d-none'); 172 | chatForm.classList.remove('d-none'); 173 | chatBody.classList.add('d-none'); 174 | chatFooter.classList.add('d-none'); 175 | } 176 | 177 | function checkValue(value) { 178 | return value == null || value.trim() == ''; 179 | } 180 | 181 | function sendNewMessage() { 182 | var isEmpty = checkValue(textareaMessage.value); 183 | 184 | if (isEmpty) { 185 | setStatus('Please enter message'); 186 | return; 187 | } 188 | 189 | sendMessage('sendMessage', textareaMessage.value); 190 | textareaMessage.value = ''; 191 | textareaMessage.focus(); 192 | } 193 | 194 | document.addEventListener('DOMContentLoaded', init); 195 | --------------------------------------------------------------------------------