Today we want you to show how you can provide live tracking charts for Web-based dashboards. This small showcase Application is based on Wicket 6, Wicket Websockets and Ubercharts.
Just imagine you are running a high-frequency Webshop and you released new Features. And you want to see the feedback what have this features bring to you as soon as possible. Normally you have to wait up to 24h to get the the reports generated through your Business Intelligence Tools. To get reports immediately you have to provide dashboards which updates themselves automatically and which are available from everywhere. This requirements we solved with Wicket a Java Webframework for building Websites and Ubercharts. Ubercharts is meant to be a tiny wrapper for apache wicket framework around the highcharts javascript library. The communication between the Wicket and Live Ubercharts will be provided by Wicket Websockets. In this post we show only the frontend part, not the backend part with saving tracking events.
Run the demo:
- Clone GitHub Repo: git clone git@github.com:comsysto/Ubercharts.git
- Go to the Ubercharts folder : cd Ubercharts
- git checkout wicketWebSockets
- gradle idea / gradle eclipse
- In IntelliJ IDEA or Eclipse open com.comsysto.runner.Start
- Start the main class
What should you see at the end :
Relevant Java Classes Diagram:
The Webapplication starts with a DemoPage.java which only creates the DownloadChartModel.java and transfer it to the DownloadChartPanel.java class which handles the Websockets communication between server and the browser.
DownloadChartModel.java creates the charts with the Uberchart library, for initialize and show the chart is the initChart() method enough. As you can see we initialize the Series with emtyArray, because the data will bee update through Websockets in the DownloadChartPanel.java class.
private Highchart initChart() { Number[] emptyArray = {}; ISeries<Number[]> rock = new NumberSeries( MusikGenre.ROCK.getName()).setData(emptyArray); ISeries<Number[]> urban = new NumberSeries( MusikGenre.URBAN.getName()).setData(emptyArray).setVisible(false); ...... Highchart highchart = new Highchart(new BarChart(), rock, pop, urban, electronic, bluesJazz); return highchart; }
For the Websocket communication we have to set events for the charts. The update events we add through highchart.getChart().getEvents().setLoad(getScript()); method the javascript file DownloadsChartPanel.js. This javascript file subscribes for Websocket messages and updates the chart:
Wicket.Event.subscribe("/websocket/open", function(jqEvent) { // show the initial state of the chart }); Wicket.Event.subscribe("/websocket/message", function(jqEvent, message) { // new record is pushed by the server var record = jQuery.parseJSON(message); if (record) { if(record.type == '${messageType}'){ columnChartUpdate(record.data, record.dataName); } if(record.type == '${messageSeriesType}'){ columnChartCategoriesUpdate(record.data, record.dataName); } } });
Websockets handling is initialized in the DownloadChartPanel.java inner class ChartUpdatingBehavior. This inner class creates an UpdateTask.java which handles the WebSocketsConnectionRegistry and the connection handling in a separate thread:
@Override public void run() { IWebSocketConnectionRegistry webSocketConnectionRegistry = new SimpleWebSocketConnectionRegistry(); while (true) { Application application = Application.get(applicationName); IWebSocketConnection connection = webSocketConnectionRegistry. getConnection(application, sessionId, pageId); if (connection == null || !connection.isOpen()) { // stop if the web socket connection is closed return; } try { updateFunction(connection); // sleep for a while to simulate work TimeUnit.SECONDS.sleep(1); } catch (Exception x) { x.printStackTrace(); return; } } }
In DownloadChartPanel we have to implement UpdateTask abstract method updateFunction() in order to update update Chart with new data. UpdateTask sends every second a message with the new values for the chart in the updateFunction() method. The Data is here generated random in the method getDownloads(), there should be a DB or Service call instead for real data. This Websocket Message will bee handled from chart events javascript functions which are loaded in DownloadChartModel.java.
private class ChartUpdatingBehavior extends WebSocketBehavior { @Override protected void onConnect(ConnectedMessage message) { super.onConnect(message); // create an asynchronous task that will write the data to the client UpdateTask updateTask = new UpdateTask(message.getApplication(), message.getSessionId(), message.getPageId()) { @Override protected void updateFunction(IWebSocketConnection connection) throws IOException { ObjectMapper objectMapper = new ObjectMapper(); Message<Number[] > message = new Message<Number[]>( MessageType.SERIES_UPDATE,selectedGenreType.name(), getDownloads()); String json = objectMapper.writeValueAsString(message); connection.sendMessage(json); } }; Executors.newScheduledThreadPool(1).schedule(updateTask, 1, TimeUnit.SECONDS); }
So now we have one way communication from the Server to the Client. Now we want to change the Music genre in the webfronted with a click on the Chart Legend. For that we add in the ChartModel the ChartCategorySwitch.js which is called on chart click function. The Script sends Wicket.Websocket message to the Server with the name of the selected genre.
function productSwitchUpdate (event) { var selected = this.index; var allSeries = this.chart.series; $.each(allSeries, function(index, series) { if(selected == index ){ series.show() ; var message='{"type":"'+'${messageType}'+'","dataName":"'+series.name+'"}' Wicket.WebSocket.send(message); }else{ series.hide(); } }); return false; }
The Server hadles the click-message in the DownloadsChartPanel.java getWebSocketBehaviorForClicks() method. This method Listens for Websockets Messages and when a GENRE_UPDATE Message comes in selectedGenreType will be set. When a Websocket message will be pushed back to the chart with the new categories names.
private Behavior getWebSocketBehaviorForClicks() { return new WebSocketBehavior() { @Override protected void onMessage(WebSocketRequestHandler handler,TextMessage message){ try{ ObjectMapper objectMapper = new ObjectMapper(); Message parsedMsg = objectMapper.readValue(message.getText(), new TypeReference() {}); if (parsedMsg.getType() == MessageType.GENRE_UPDATE){ selectedGenreType = MusikGenre.valueOf( parsedMsg.getDataName().toUpperCase()); Message<String[]> msg = new Message<String[]>( MessageType.CATEGORIES_UPDATE,selectedGenreType.name(), musikGenreMap.get(selectedGenreType)); String json = objectMapper.writeValueAsString(msg); handler.push(json); } } catch (IOException e) { e.printStackTrace(); } } }; }
While developing we have some problems to get it running in Jetty and Tomcat with the same war file. For fixing it you have to add dependencies in the build.gradle file:
"org.apache.wicket:wicket-native-websocket-jetty:0.6", "org.apache.wicket:wicket-native-websocket-tomcat:0.6",
and also add a filter in web.xml file:
//ENABLE WHEN JETTY <filter-class>org.apache.wicket.protocol.ws.jetty.Jetty7WebSocketFilter</filter-class> //ENABLE WHEN TOMCAT <filter-class>org.apache.wicket.protocol.ws.tomcat7.Tomcat7WebSocketFilter</filter-class>
Next steps are migrating it to jetty 9 and add some Wrapper for Ubercharts to easier add Updating tasks.
If you have any feedback, please write to luka.leovac@comsysto.com or alen.tolj@comsysto.com
