This post is part of the series of posts covering different JavaEE 7 features. In the past I wrote about:
- Creating WebSocket using JavaEE 7
- Parsing JSON using the JSR 353 implementation in JavaEE 7- Using the Streaming API.
- Parsing JSON using the JSR 353 implementation in JavaEE 7- Using the Object Model API.
This post is an extension of the WebSocket in Java EE 7 and explores another feature supported with WebSockets in JavaEE 7. This feature allows a WebSocket to be wrapped around Decoders and Encoders. The task of Decoders is to intercept the incoming message and decode into a form specified by the Decoders. The WebSocket then makes used of the decoded form to perform further processing. The task of Encoders is the reverse of Decoders, they take the output from the WebSocket and encode into a format which is then sent to the client end point. I have used the plural forms i.e Decoders and Encoders because one can specify multiple Decoders and Encoders for a WebSocket.
In this post I will show you how to create a Decoder which takes a JSON String message and decodes into Java objects.
In one of my previous posts I wrote about Parsing JSON using Streaming API and in that I have used an example from the book Head First HTML5. I am going to reuse the same example in this post as well. So the following sections will be the same in this post as well:
- Storing the sticky notes in the browser local storage
- Creating JSON representation of all the sticky notes
So one has to copy the client side related code namely- The JSP file: notetoself.jsp, The JavaScript file: notetoself.js, The CSS file: notetoself.css. The code which is going to change is the Server side code i.e the Server side WebSocket end point. The Server Side end point code as given in the other post is:
import java.io.StringReader; import java.util.HashMap; import java.util.Map; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; import javax.json.JsonReader; import javax.json.JsonValue; import javax.json.stream.JsonParser; import static javax.json.stream.JsonParser.Event.END_OBJECT; import static javax.json.stream.JsonParser.Event.KEY_NAME; import static javax.json.stream.JsonParser.Event.VALUE_STRING; import javax.websocket.OnMessage; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; /** * Demo for parsing the JSON using Streaming API. */ @ServerEndpoint("/saveStickNotes") public class TodoApplicationEndPoint { private Map<String, String> stickyNotes; @OnMessage public void receiveMessage(String message, Session session){ parseUsingStreamingAPI(message); } private void parseUsingStreamingAPI(String message) { JsonParser parser = Json.createParser(new StringReader(message)); String keyType = null; String key = null, value = null; stickyNotes = new HashMap<>(); while(parser.hasNext()){ //Identify the type of event i.e type of JSON element. switch (parser.next()){ //If the JSON element is the "KEY", then record the key type. case KEY_NAME: keyType = parser.getString(); break; //If the JSON element is a value string then extract the value //and decide if the value is a sticky note key or sticky note data. case VALUE_STRING: switch(keyType){ case "key": key = parser.getString(); case "value": value = parser.getString(); } break; //Once we encounter the end of the sticky note object, update the //map with the sticky note key and sticky note value. case END_OBJECT: stickyNotes.put(key, value); break; } } //Printing the sticky note data to verify that we have correctly parsed the data. for(String k : stickyNotes.keySet()){ System.out.println("Key: "+k+": "+ stickyNotes.get(k)); } } }
We are going to move the code from Line 30 upto Line 63 (highlighted in the code above) into a Decoder class. And in the same highlighted code above we are making use of Streaming API to parse the JSON, but we will replace that and make use of the Object Model AP to parse the data. If you want to see how to use Object Model API to parse JSON data, please refer to this post.
Creating a Decoder class
There are 2 things which are to be done to get the decoder working:
- Create a class which acts as a Decoder.
- Register this class with the WebSocket indicating that it is the decoder to be used.
Lets look at the 2 things one after the other.
Create a class which acts as a Decoder
There are 5 interfaces which one has to be familiar with:
- Decoder: The Decoder interface holds member interfaces that define how a developer can provide the web socket container a way web socket messages into developer defined custom objects.
- Decoder.Binary<T>: This interface defines how a custom object (of type T) is decoded from a web socket message in the form of a byte buffer.
- Decoder.BinaryStream<T>: This interface defines how a custom object is decoded from a web socket message in the form of a binary stream.
- Decoder.Text<T>: This interface defines how a custom object is decoded from a web socket message in the form of a string.
- Decoder.TextStream<T>: This interface defines how a custom object of type T is decoded from a web socket message in the form of a character stream.
In this example we will be making use of the Decoder.Text
interface. The Decoder interface has following methods:
- destroy(): This method is called when the decoder is about to be removed from service in order that any resources the encoder used may be closed gracefully.
- init(EndpointConfig config): This method is called with the endpoint configuration object of the endpoint this decoder is intended for when it is about to be brought into service.
These are the service related methods. The Decoder.Text<T>
interface has the following methods in addition to the methods it inherits from Decoder
interface:
- decode(String s): Decode the given String into an object of type T.
- willDecode(String s): Answer whether the given String can be decoded into an object of type T.
Out of the above 4 methods we are more concerned with the methods declared in Decoder.Text<T>
interface. The <T> generic type indicates the return type of the decoder i.e the type returned by the decoder after decoding the message it receives from the client end point. In this example the decoder returns an instance of List<MyStickyNote>
. Lets look at the MyStickyNote
class definition which is used to hold the data for a single sticky note:
import java.util.Date; import javax.json.JsonObject; /** * Holds the data required for a single sticky note. */ public class MyStickyNote { /** * The Date and time when the sticky note was received at the server. */ private Date receivedAt; /** * The JSON object which holds the data in a key-value pairs. * We fetch the key and the sticky note text from thi JSON Object. */ private JsonObject stickyNoteObj; public MyStickyNote() { } public MyStickyNote(Date receivedAt, JsonObject stickyNoteObj) { this.receivedAt = receivedAt; this.stickyNoteObj = stickyNoteObj; } /** * Extracts and returns the sticky note text which is present in the JSON object. */ public String getStickyNote(){ return getStickyNoteObj().getString("value"); } /** * Extracts and returns the key to identify the sticky note. * Again this data is present in the JSON object. */ public String getKey(){ return getStickyNoteObj().getString("key"); } public Date getReceivedAt() { return receivedAt; } public void setReceivedAt(Date receivedAt) { this.receivedAt = receivedAt; } public JsonObject getStickyNoteObj() { return stickyNoteObj; } public void setStickyNoteObj(JsonObject stickyNoteObj) { this.stickyNoteObj = stickyNoteObj; } @Override public String toString(){ return getStickyNote() +" received at "+ receivedAt.toString(); } }
Now that we have defined our model class, lets look at the decoder class which intercepts the incoming messages and then decodes them into List<MyStickyNote>
.
import java.io.StringReader; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.json.Json; import javax.json.JsonArray; import javax.json.JsonObject; import javax.websocket.DecodeException; import javax.websocket.Decoder; import javax.websocket.EndpointConfig; public class StickyNoteDecoder implements Decoder.Text<List<MyStickyNote>>{ /** * This method intercepts the incoming data to the websocket and * performs the required processing as defined in the method and then * returns the result to the websocket. */ @Override public List<MyStickyNote> decode(String message) throws DecodeException { /** * To understand how this parsing works please refer * to the detailed explanation in: * https://javabeat.net/2013/10/json-processing-javaee-7-object-model-api/ */ JsonArray jsonArray = Json.createReader(new StringReader(message)) .readArray(); List<MyStickyNote> stickyNotes = new ArrayList<>(); for(int i = 0; i < jsonArray.size(); i++){ JsonObject jsonObject = jsonArray.getJsonObject(i); MyStickyNote note = new MyStickyNote(new Date(), jsonObject); stickyNotes.add(note); } return stickyNotes; } /** * Indicates that whether this decoder will decode messages of the type * accepted by the method parameter which in this case is String. */ @Override public boolean willDecode(String arg0) { return true; } /** * This is a decoder lifecycle method which we are not modifying or overriding */ @Override public void init(EndpointConfig config) { } /** * This is a decoder lifecycle method which we are not modifying or overriding */ @Override public void destroy() { } }
In the above code the implementation of decode(String message)
does the task of parsing the JSON message and building the List
of MyStickyote
objects. Now that we have a decoder in place, our next task will be to register this decoder with the WebSocket.
Registering the Decoder with the WebSocket
This is fairly simple and there are many ways of doing this. As we are using annotated approach to creating WebSockets we will use the same approach and register the decoder class by passing the class name as an argument to the annotation. The highlighted code below shows the way to register the decoder. Also note in the code below that we have moved all the JSON parsing logic into the decoder and the input type to the WebSocket method has changed to the return type of the decoder.
import java.util.List; import java.util.Map; import javax.websocket.OnMessage; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; /** * Demo for parsing the JSON using Streaming API. */ @ServerEndpoint(value = "/saveStickNotes", decoders = {StickyNoteDecoder.class}) public class TodoApplicationEndPoint { private Map<String, String> stickyNotes; @OnMessage public void receiveListMessage(List<MyStickyNote> stickyNotes, Session session){ for(MyStickyNote note : stickyNotes){ System.out.println(note); } } }
Compare the two different definitions of the same class TodoApplicationEndPoint
– the one given above and the one given towards the beginning of the post. The latter one i.e the one given above is more concise and confirms to handling Single Responsibility i.e to business operations on the processed data. The parsing or processing of the incoming data is handled by the decoder and the decoder also confirm to handling single responsibility. This way the code is much cleaner then the one given towards the beginning.
To summarize:
- The client side code for this post remains the same as given in the post here.
- The server side code i.e the
TodoApplicationEndPoint
class definition has changed and been trimmed down. - Few new class namely:
MyStickyNote
andStickyNoteDecoder
have been added.
If you are stuck understanding the example, feel free to drop in your issues as comments.