├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src └── main └── java └── io └── github └── benas └── websocket ├── client ├── Client.java └── ClientEndpoint.java ├── model ├── Message.java ├── MessageDecoder.java └── MessageEncoder.java ├── server ├── Server.java └── ServerEndpoint.java └── util └── JsonUtil.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | *.iws 5 | 6 | # Mac 7 | .DS_Store 8 | 9 | # Maven 10 | log/ 11 | target/ 12 | 13 | *.class 14 | 15 | # Mobile Tools for Java (J2ME) 16 | .mtj.tmp/ 17 | 18 | # Package Files # 19 | *.jar 20 | *.war 21 | *.ear 22 | 23 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 24 | hs_err_pid* 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mahmoud Ben Hassine 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 | In this repository, I'll use Web sockets to create a tiny chat server using [Tyrus](https://tyrus.java.net/), the reference implementation of the Java API for WebSocket (JSR 356). A great introduction to this API can be found on Oracle Network [here](https://www.oracle.com/technical-resources/articles/java/jsr356.html). 2 | 3 | In order to keep things simple, the server and clients will be command line apps, no GUIs here, it is serious stuff :smile: So let's get started! 4 | 5 | ## A quick introduction to the Java API for WebSocket 6 | 7 | If you don't have time to check all details of this API, here are the most important points to know. JSR 356 provides: 8 | 9 | * both a programmatic and a declarative way to define client and server endpoints. In this post, I'll use the declarative way, through annotations. 10 | * lifecyle events to which we can listen: open/close a session, send/receive a message, etc 11 | * message encoder/decoder to marshal/unmarshall (binary or character) messages between clients and servers. 12 | Encoders/Decorders are important since they allow us to use Java objects as messages instead of dealing with raw data that transit over the wire. We will see an example in a couple of minutes 13 | 14 | That's all we need to know for now in order to implement the chat application. 15 | 16 | ## The data model 17 | 18 | Since we will be creating a chat server, let's first model messages with a POJO: 19 | 20 | ```java 21 | public class Message { 22 | 23 | private String content; 24 | private String sender; 25 | private Date received; 26 | 27 | // getters and setters 28 | } 29 | ``` 30 | 31 | Messages will be encoded/decoded to JSON format between the server and clients using the following `Encoder/Decoder` classes: 32 | 33 | ```java 34 | public class MessageEncoder implements Encoder.Text { 35 | 36 | @Override 37 | public String encode(final Message message) throws EncodeException { 38 | return Json.createObjectBuilder() 39 | .add("message", message.getContent()) 40 | .add("sender", message.getSender()) 41 | .add("received", "") 42 | .build().toString(); 43 | } 44 | 45 | } 46 | ``` 47 | 48 | ```java 49 | public class MessageDecoder implements Decoder.Text { 50 | 51 | @Override 52 | public Message decode(final String textMessage) throws DecodeException { 53 | Message message = new Message(); 54 | JsonObject jsonObject = Json.createReader(new StringReader(textMessage)).readObject(); 55 | message.setContent(jsonObject.getString("message")); 56 | message.setSender(jsonObject.getString("sender")); 57 | message.setReceived(new Date()); 58 | return message; 59 | } 60 | 61 | } 62 | ``` 63 | 64 | The `MessageEncoder` uses the [Java API for JSON Processing](https://jsonp.java.net/) to create Json objects, no additional libraries :wink: That's all for the data model, we have created our domain model object `Message`, and two utility classes to serialize/deserialize messages to/from JSON format. 65 | 66 | ## The server side 67 | 68 | The following is the code of the server endpoint. I'll explain it in details right after the listing: 69 | 70 | ```java 71 | @ServerEndpoint(value = "/chat", encoders = MessageEncoder.class, decoders = MessageDecoder.class) 72 | public class ServerEndpoint { 73 | 74 | static Set peers = Collections.synchronizedSet(new HashSet()); 75 | 76 | @OnOpen 77 | public void onOpen(Session session) { 78 | System.out.println(format("%s joined the chat room.", session.getId())); 79 | peers.add(session); 80 | } 81 | 82 | @OnMessage 83 | public void onMessage(Message message, Session session) throws IOException, EncodeException { 84 | //broadcast the message 85 | for (Session peer : peers) { 86 | if (!session.getId().equals(peer.getId())) { // do not resend the message to its sender 87 | peer.getBasicRemote().sendObject(message); 88 | } 89 | } 90 | } 91 | 92 | @OnClose 93 | public void onClose(Session session) throws IOException, EncodeException { 94 | System.out.println(format("%s left the chat room.", session.getId())); 95 | peers.remove(session); 96 | //notify peers about leaving the chat room 97 | for (Session peer : peers) { 98 | Message message = new Message(); 99 | message.setSender("Server"); 100 | message.setContent(format("%s left the chat room", (String) session.getUserProperties().get("user"))); 101 | message.setReceived(new Date()); 102 | peer.getBasicRemote().sendObject(message); 103 | } 104 | } 105 | 106 | } 107 | ``` 108 | 109 | The code is self explanatory, but here are the important points to note: 110 | 111 | * The `ServerEndpoint` annotation marks the class as a server endpoint. We need to specify the web socket endpoint as well as message encoder/decoder. 112 | * We need to hold a set of connected users in order to broadcast messages to chat rooms 113 | * All lifecycle event methods have a `Session` parameter that allows to get a handle of the user that initiated the action (join/leave/message) 114 | * `MessageEncoder` and `MessageDecoder` are used to marshal/unmarshal messages from JSON to our `Message` class. This is what allows us to have a parameter of type 115 | `Message` in the `onMessage` method. Without these encoder/decoder classes, we need to handle raw text messages.. 116 | 117 | That's all for the server side, nothing fancy :smile: 118 | 119 | ## The client side 120 | 121 | On the client side, we need to listen to the `onMessage` event and print the received message to the console: 122 | 123 | ```java 124 | @ClientEndpoint(encoders = MessageEncoder.class, decoders = MessageDecoder.class) 125 | public class ClientEndpoint { 126 | 127 | private SimpleDateFormat simpleDateFormat = new SimpleDateFormat(); 128 | 129 | @OnMessage 130 | public void onMessage(Message message) { 131 | System.out.println(String.format("[%s:%s] %s", 132 | simpleDateFormat.format(message.getReceived()), message.getSender(), message.getContent())); 133 | } 134 | 135 | } 136 | ``` 137 | 138 | ## Putting it all together 139 | 140 | Now that we have defined the data model, client and server endpoints, we can proceed with a final main class to run the application. The main class to run the server is the following: 141 | 142 | ```java 143 | public class Server { 144 | 145 | public static void main(String[] args) { 146 | org.glassfish.tyrus.server.Server server = 147 | new org.glassfish.tyrus.server.Server("localhost", 8025, "/ws", ServerEndpoint.class); 148 | try { 149 | server.start(); 150 | System.out.println("Press any key to stop the server.."); 151 | new Scanner(System.in).nextLine(); 152 | } catch (DeploymentException e) { 153 | throw new RuntimeException(e); 154 | } finally { 155 | server.stop(); 156 | } 157 | } 158 | 159 | } 160 | ``` 161 | 162 | All we have to do is create a `org.glassfish.tyrus.server.Server` instance and start it. 163 | We should specify the `ServerEndpoint` class, the port and the websocket endpoint as parameters. To create a client, I'll use the following class: 164 | 165 | ```java 166 | public class Client { 167 | 168 | public static final String SERVER = "ws://localhost:8025/ws/chat"; 169 | 170 | public static void main(String[] args) throws Exception { 171 | ClientManager client = ClientManager.createClient(); 172 | String message; 173 | 174 | // connect to server 175 | Scanner scanner = new Scanner(System.in); 176 | System.out.println("Welcome to Tiny Chat!"); 177 | System.out.println("What's your name?"); 178 | String user = scanner.nextLine(); 179 | Session session = client.connectToServer(ClientEndpoint.class, new URI(SERVER)); 180 | System.out.println("You are logged in as: " + user); 181 | 182 | // repeatedly read a message and send it to the server (until quit) 183 | do { 184 | message = scanner.nextLine(); 185 | session.getBasicRemote().sendText(formatMessage(message, user)); 186 | } while (!message.equalsIgnoreCase("quit")); 187 | } 188 | 189 | } 190 | ``` 191 | 192 | The `ClientManager` provided by JSR 356 is the entry point to create clients. Once we get a client, it is possible to connect to the server and obtain a `Session` object. This is the main point of interaction between server and clients. It allows to send messages, register handlers and get/set sessions parameters. 193 | 194 | ## Build from source 195 | 196 | ```bash 197 | $> git clone https://github.com/benas/web-socket-lab.git 198 | $> cd web-socket-lab 199 | $> mvn package 200 | ``` 201 | 202 | ## Start the server 203 | 204 | Open a terminal and run the following command in the `target` directory: 205 | 206 | ```bash 207 | $> java -cp "web-socket-lab-1.0-SNAPSHOT.jar:lib/*" io.github.benas.websocket.server.Server 208 | ``` 209 | 210 | _If you are on Windows, make sure to change the classpath separator to ; instead of :_ 211 | 212 | ## Launch a first client 213 | 214 | Open a second terminal and run the following command in the `target` directory: 215 | 216 | ```bash 217 | $> java -cp "web-socket-lab-1.0-SNAPSHOT.jar:lib/*" io.github.benas.websocket.client.Client 218 | ``` 219 | 220 | _If you are on Windows, make sure to change the classpath separator to ; instead of :_ 221 | 222 | ## Launch a second client 223 | 224 | With the same command used to launch the first client, run a second client in a separate terminal and start chatting :smile: 225 | 226 | # Example 227 | 228 | ### Server 229 | 230 | ``` 231 | INFO: Provider class loaded: org.glassfish.tyrus.container.grizzly.GrizzlyEngine 232 | INFO: Started listener bound to [0.0.0.0:8025] 233 | INFO: [HttpServer] Started. 234 | INFO: WebSocket Registered apps: URLs all start with ws://localhost:8025 235 | INFO: WebSocket server started. 236 | Please press a key to stop the server. 237 | INFO: 7718a10e-d965-48ce-a324-44996322d469 joined the chat room. 238 | INFO: 1c721ade-74e6-4de2-8d26-53e74a126cb8 joined the chat room. 239 | INFO: [7718a10e-d965-48ce-a324-44996322d469:Wed Oct 15 22:07:04 CEST 2014] hi there! 240 | INFO: [1c721ade-74e6-4de2-8d26-53e74a126cb8:Wed Oct 15 22:07:25 CEST 2014] fine? 241 | INFO: [7718a10e-d965-48ce-a324-44996322d469:Wed Oct 15 22:07:33 CEST 2014] yeah! 242 | INFO: 1c721ade-74e6-4de2-8d26-53e74a126cb8 left the chat room. 243 | INFO: 7718a10e-d965-48ce-a324-44996322d469 left the chat room. 244 | 245 | INFO: Stopped listener bound to [0.0.0.0:8025] 246 | INFO: Websocket Server stopped. 247 | ``` 248 | 249 | ### Client 1 250 | 251 | ``` 252 | Welcome to Tiny Chat! 253 | What' your name? 254 | benas 255 | Connection established. session id: 79b967e0-3dc0-40dd-b380-71af2d7adfe3 256 | You are logged in as benas 257 | [benas] hi there! 258 | [10/15/14 10:07 PM:tom] fine? 259 | [benas] yeah! 260 | [10/15/14 10:07 PM:Server] tom left the chat room. 261 | [benas] quit 262 | ``` 263 | 264 | ### Client 2 265 | 266 | ``` 267 | Welcome to Tiny Chat! 268 | What' your name? 269 | tom 270 | Connection established. session id: 8824899f-5b6f-4d58-8054-c896e414ceef 271 | You are logged in as tom 272 | [10/15/14 10:07 PM:benas] hi there! 273 | [tom] fine? 274 | [10/15/14 10:07 PM:benas] yeah! 275 | quit 276 | ``` 277 | 278 | ## Summary 279 | 280 | The goal of this repo is to show how to create a chat application using the Java API for WebSocket (JSR 356). This API has been added in Java EE 7 and greatly simplifies the programming model of real-time interactive applications. Note that I've used only reference implementations of Java EE APIs, without using a container or any third party API or library. 281 | 282 | I've already used a Javascript stack for this kind of requirements, basically to implement a more elaborate [real-time game server](https://github.com/benas/gamehub.io) with NodeJs and SocketIO. I must admit that the Java API is also really great, easy and straightforward. 283 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.benas 8 | web-socket-lab 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | javax.websocket 14 | javax.websocket-api 15 | 1.0 16 | 17 | 18 | 19 | org.glassfish.tyrus 20 | tyrus-server 21 | 1.1 22 | 23 | 24 | org.glassfish.tyrus 25 | tyrus-client 26 | 1.1 27 | 28 | 29 | 30 | org.glassfish.tyrus 31 | tyrus-container-grizzly 32 | 1.1 33 | 34 | 35 | 36 | org.glassfish 37 | javax.json 38 | 1.0.4 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.1 49 | 50 | 1.7 51 | 1.7 52 | 1.7 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-dependency-plugin 58 | 59 | 60 | copy-dependencies 61 | prepare-package 62 | 63 | copy-dependencies 64 | 65 | 66 | ${project.build.directory}/lib 67 | true 68 | true 69 | true 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/client/Client.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.client; 2 | 3 | import static io.github.benas.websocket.util.JsonUtil.formatMessage; 4 | 5 | import java.net.URI; 6 | import java.util.Scanner; 7 | import javax.websocket.Session; 8 | import org.glassfish.tyrus.client.ClientManager; 9 | 10 | public class Client { 11 | 12 | public static final String SERVER = "ws://localhost:8025/ws/chat"; 13 | 14 | public static void main(String[] args) throws Exception { 15 | ClientManager client = ClientManager.createClient(); 16 | String message; 17 | 18 | // connect to server 19 | Scanner scanner = new Scanner(System.in); 20 | System.out.println("Welcome to Tiny Chat!"); 21 | System.out.println("What's your name?"); 22 | String user = scanner.nextLine(); 23 | Session session = client.connectToServer(ClientEndpoint.class, new URI(SERVER)); 24 | System.out.println("You are logged in as: " + user); 25 | 26 | // repeatedly read a message and send it to the server (until quit) 27 | do { 28 | message = scanner.nextLine(); 29 | session.getBasicRemote().sendText(formatMessage(message, user)); 30 | } while (!message.equalsIgnoreCase("quit")); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/client/ClientEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.client; 2 | 3 | import static java.lang.String.format; 4 | 5 | import io.github.benas.websocket.model.Message; 6 | import io.github.benas.websocket.model.MessageDecoder; 7 | import io.github.benas.websocket.model.MessageEncoder; 8 | 9 | import javax.websocket.OnMessage; 10 | import javax.websocket.OnOpen; 11 | import javax.websocket.Session; 12 | import java.text.SimpleDateFormat; 13 | 14 | @javax.websocket.ClientEndpoint(encoders = MessageEncoder.class, decoders = MessageDecoder.class) 15 | public class ClientEndpoint { 16 | 17 | private SimpleDateFormat simpleDateFormat = new SimpleDateFormat(); 18 | 19 | @OnOpen 20 | public void onOpen(Session session) { 21 | System.out.println(format("Connection established. session id: %s", session.getId())); 22 | } 23 | 24 | @OnMessage 25 | public void onMessage(Message message) { 26 | System.out.println(format("[%s:%s] %s", simpleDateFormat.format(message.getReceived()), message.getSender(), message.getContent())); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/model/Message.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.model; 2 | 3 | import java.util.Date; 4 | 5 | public class Message { 6 | 7 | private String content; 8 | private String sender; 9 | private Date received; 10 | 11 | public final String getContent() { 12 | return content; 13 | } 14 | 15 | public final void setContent(final String content) { 16 | this.content = content; 17 | } 18 | 19 | public final String getSender() { 20 | return sender; 21 | } 22 | 23 | public final void setSender(final String sender) { 24 | this.sender = sender; 25 | } 26 | 27 | public final Date getReceived() { 28 | return received; 29 | } 30 | 31 | public final void setReceived(final Date received) { 32 | this.received = received; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/model/MessageDecoder.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.model; 2 | 3 | import javax.json.Json; 4 | import javax.json.JsonObject; 5 | import javax.websocket.DecodeException; 6 | import javax.websocket.Decoder; 7 | import javax.websocket.EndpointConfig; 8 | import java.io.StringReader; 9 | import java.util.Date; 10 | 11 | public class MessageDecoder implements Decoder.Text { 12 | 13 | @Override 14 | public void init(final EndpointConfig config) { 15 | } 16 | 17 | @Override 18 | public void destroy() { 19 | } 20 | 21 | @Override 22 | public Message decode(final String textMessage) throws DecodeException { 23 | Message message = new Message(); 24 | JsonObject jsonObject = Json.createReader(new StringReader(textMessage)).readObject(); 25 | message.setContent(jsonObject.getString("message")); 26 | message.setSender(jsonObject.getString("sender")); 27 | message.setReceived(new Date()); 28 | return message; 29 | } 30 | 31 | @Override 32 | public boolean willDecode(final String s) { 33 | return true; 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/model/MessageEncoder.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.model; 2 | 3 | import io.github.benas.websocket.util.JsonUtil; 4 | 5 | import javax.websocket.EncodeException; 6 | import javax.websocket.Encoder; 7 | import javax.websocket.EndpointConfig; 8 | 9 | public class MessageEncoder implements Encoder.Text { 10 | 11 | @Override 12 | public void init(final EndpointConfig config) { 13 | } 14 | 15 | @Override 16 | public void destroy() { 17 | } 18 | 19 | @Override 20 | public String encode(final Message message) throws EncodeException { 21 | return JsonUtil.formatMessage(message.getContent(), message.getSender()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/server/Server.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.server; 2 | 3 | import java.util.Scanner; 4 | import javax.websocket.DeploymentException; 5 | 6 | public class Server { 7 | 8 | public static void main(String[] args) { 9 | 10 | org.glassfish.tyrus.server.Server server = new org.glassfish.tyrus.server.Server("localhost", 8025, "/ws", ServerEndpoint.class); 11 | 12 | try { 13 | server.start(); 14 | System.out.println("Press any key to stop the server.."); 15 | new Scanner(System.in).nextLine(); 16 | } catch (DeploymentException e) { 17 | throw new RuntimeException(e); 18 | } finally { 19 | server.stop(); 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/server/ServerEndpoint.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.server; 2 | 3 | import static java.lang.String.format; 4 | 5 | import java.io.IOException; 6 | import java.util.Collections; 7 | import java.util.Date; 8 | import java.util.HashSet; 9 | import java.util.Set; 10 | import javax.websocket.EncodeException; 11 | import javax.websocket.OnClose; 12 | import javax.websocket.OnMessage; 13 | import javax.websocket.OnOpen; 14 | import javax.websocket.Session; 15 | 16 | import io.github.benas.websocket.model.Message; 17 | import io.github.benas.websocket.model.MessageDecoder; 18 | import io.github.benas.websocket.model.MessageEncoder; 19 | 20 | @javax.websocket.server.ServerEndpoint(value = "/chat", encoders = MessageEncoder.class, decoders = MessageDecoder.class) 21 | public class ServerEndpoint { 22 | 23 | static Set peers = Collections.synchronizedSet(new HashSet()); 24 | 25 | @OnOpen 26 | public void onOpen(Session session) { 27 | System.out.println(format("%s joined the chat room.", session.getId())); 28 | peers.add(session); 29 | } 30 | 31 | @OnMessage 32 | public void onMessage(Message message, Session session) throws IOException, EncodeException { 33 | String user = (String) session.getUserProperties().get("user"); 34 | if (user == null) { 35 | session.getUserProperties().put("user", message.getSender()); 36 | } 37 | if ("quit".equalsIgnoreCase(message.getContent())) { 38 | session.close(); 39 | } 40 | 41 | System.out.println(format("[%s:%s] %s", session.getId(), message.getReceived(), message.getContent())); 42 | 43 | //broadcast the message 44 | for (Session peer : peers) { 45 | if (!session.getId().equals(peer.getId())) { // do not resend the message to its sender 46 | peer.getBasicRemote().sendObject(message); 47 | } 48 | } 49 | } 50 | 51 | @OnClose 52 | public void onClose(Session session) throws IOException, EncodeException { 53 | System.out.println(format("%s left the chat room.", session.getId())); 54 | peers.remove(session); 55 | //notify peers about leaving the chat room 56 | for (Session peer : peers) { 57 | Message chatMessage = new Message(); 58 | chatMessage.setSender("Server"); 59 | chatMessage.setContent(format("%s left the chat room.", (String) session.getUserProperties().get("user"))); 60 | chatMessage.setReceived(new Date()); 61 | peer.getBasicRemote().sendObject(chatMessage); 62 | } 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/main/java/io/github/benas/websocket/util/JsonUtil.java: -------------------------------------------------------------------------------- 1 | package io.github.benas.websocket.util; 2 | 3 | import javax.json.Json; 4 | 5 | public class JsonUtil { 6 | 7 | public static String formatMessage(String message, String user) { 8 | return Json.createObjectBuilder() 9 | .add("message", message) 10 | .add("sender", user) 11 | .add("received", "") 12 | .build().toString(); 13 | } 14 | 15 | } 16 | --------------------------------------------------------------------------------