Skip to content

Instantly share code, notes, and snippets.

@nicholashagen
Created September 2, 2012 20:10
Show Gist options
  • Save nicholashagen/3604120 to your computer and use it in GitHub Desktop.
Save nicholashagen/3604120 to your computer and use it in GitHub Desktop.
Server-Sent Events in Servlets
<!doctype html>
<html>
<head>
<title>Long Task Example</title>
<style type="text/css">
/* CSS3 Progress Bar based on */
/* http://www.catswhocode.com/blog/how-to-create-a-kick-ass-css3-progress-bar */
@-webkit-keyframes animate-stripes {
to { background-position: 0 0; }
from { background-position: -200px 0; }
}
@-moz-keyframes animate-stripes {
to { background-position: 0 0; }
from { background-position: 36px 0; }
}
.progress {
width: 200px;
}
.ui-progress-bar {
position: relative;
height: 25px;
padding-right: 2px;
background-color: #abb2bc;
-moz-border-radius: 25px;
-webkit-border-radius: 25px;
-o-border-radius: 25px;
-ms-border-radius: 25px;
-khtml-border-radius: 25px;
border-radius: 25px;
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #949daa), color-stop(100%, #abb2bc));
background: -webkit-linear-gradient(#949daa 0%, #abb2bc 100%);
background: -moz-linear-gradient(#949daa 0%, #abb2bc 100%);
background: -o-linear-gradient(#949daa 0%, #abb2bc 100%);
background: -ms-linear-gradient(#949daa 0%, #abb2bc 100%);
background: linear-gradient(#949daa 0%, #abb2bc 100%);
-moz-box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px white;
-webkit-box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px white;
-o-box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px white;
box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px white;
}
.ui-progress-bar.transition .ui-progress {
-moz-transition: width 0.5s ease-in, background-color 0.5s ease-in, border-color 1.5s ease-out, box-shadow 1.5s ease-out;
-webkit-transition: width 0.5s ease-in, background-color 0.5s ease-in, border-color 1.5s ease-out, box-shadow 1.5s ease-out;
-o-transition: width 0.5s ease-in, background-color 0.5s ease-in, border-color 1.5s ease-out, box-shadow 1.5s ease-out;
transition: width 0.5s ease-in, background-color 0.5s ease-in, border-color 1.5s ease-out, box-shadow 1.5s ease-out;
}
.ui-progress-bar .ui-progress {
position: relative;
display: block;
overflow: hidden;
height: 23px;
-moz-border-radius: 25px;
-webkit-border-radius: 25px;
-o-border-radius: 25px;
-ms-border-radius: 25px;
-khtml-border-radius: 25px;
border-radius: 25px;
-webkit-background-size: 280px 44px;
-moz-background-size: 36px 36px;
background-color: #74d04c;
background: -webkit-linear-gradient(-30deg, rgba(255, 255, 255, 0.17), rgba(255, 255, 255, 0.17) 30px, rgba(255, 255, 255, 0) 30px, rgba(255, 255, 255, 0) 60px, rgba(255, 255, 255, 0.17) 60px, rgba(255, 255, 255, 0.17) 90px, rgba(255, 255, 255, 0) 90px, rgba(255, 255, 255, 0) 120px, rgba(255, 255, 255, 0.17) 120px, rgba(255, 255, 255, 0.17) 150px, rgba(255, 255, 255, 0) 150px, rgba(255, 255, 255, 0) 180px, rgba(255, 255, 255, 0.17) 180px, rgba(255, 255, 255, 0.17) 210px, rgba(255, 255, 255, 0) 210px, rgba(255, 255, 255, 0) 240px, rgba(255, 255, 255, 0.17) 240px, rgba(255, 255, 255, 0.17) 270px, rgba(255, 255, 255, 0) 270px, #74d04c), #74d04c;
background: -moz-repeating-linear-gradient(top left -30deg, rgba(255, 255, 255, 0.17), rgba(255, 255, 255, 0.17) 15px, rgba(255, 255, 255, 0) 15px, rgba(255, 255, 255, 0) 30px), -moz-linear-gradient(rgba(255, 255, 255, 0.25) 0%, rgba(255, 255, 255, 0) 100%), #74d04c;
-moz-box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.4), inset 0px -1px 1px rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.4), inset 0px -1px 1px rgba(0, 0, 0, 0.2);
-o-box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.4), inset 0px -1px 1px rgba(0, 0, 0, 0.2);
box-shadow: inset 0px 1px 0px 0px rgba(255, 255, 255, 0.4), inset 0px -1px 1px rgba(0, 0, 0, 0.2);
border: 1px solid #4c8932;
-moz-animation: animate-stripes 2s linear infinite;
-webkit-animation: animate-stripes 5s linear infinite;
-o-animation: animate-stripes 2s linear infinite;
-ms-animation: animate-stripes 2s linear infinite;
-khtml-animation: animate-stripes 2s linear infinite;
animation: animate-stripes 2s linear infinite;
}
.ui-progress-bar .ui-progress span.ui-label {
-moz-font-smoothing: antialiased;
-webkit-font-smoothing: antialiased;
-o-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
-khtml-font-smoothing: antialiased;
font-smoothing: antialiased;
font-size: 13px;
position: absolute;
right: 0;
line-height: 23px;
padding-right: 12px;
color: rgba(0, 0, 0, 0.6);
text-shadow: rgba(255, 255, 255, 0.45) 0 1px 0px;
white-space: nowrap;
}
</style>
</head>
<body>
<h1>SSE EVENTS</h1>
<a href="#" onclick="startTask(); return false;">Start Long Task</a><br /><br />
<!-- Progress bar adapted from http://www.catswhocode.com/blog/how-to-create-a-kick-ass-css3-progress-bar -->
<div class="progress">
<div class="ui-progress-bar ui-container transition" id="progress_bar">
<div id="progress" class="ui-progress" style="width: 0%;">
<span id="label" class="ui-label"></span>
</div>
</div>
</div>
<script type="text/javascript">
function startTask() {
/* create the event source */
var source = new EventSource('/sse/task');
/* handle incoming messages */
source.onmessage = function(event) {
if (event.type == 'message') {
// data expected to be in JSON-format, so parse */
var data = JSON.parse(event.data);
// server sends complete:true on completion
if (data.complete) {
// close the connection so browser does not keep connecting
source.close();
// update the UI now that task is complete
document.getElementById('label').innerHTML = 'Complete';
}
// otherwise, it's a progress update so just update progress bar
else {
var pct = 100.0 * data.current / data.total;
document.getElementById('progress').style.width = pct + '%';
document.getElementById('label').innerHTML = data.current + ' of ' + data.total;
}
}
};
source.onerror = function(event) {
console.log('Failed to Start EventSource: ', event);
};
}
</script>
</body>
</html>
package com.znet.examples.sse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* This is a sample servlet that demonstrates SSE inside of a servlet using
* standard GET requests to the servlet.
*/
public class LongTaskServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private ExecutorService executor;
public LongTaskServlet() {
super();
}
@Override
public void init(ServletConfig config) throws ServletException {
this.executor = Executors.newCachedThreadPool();
}
/**
* Handle the GET request by constantly sending SSE events until the task
* is complete.
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// handle base functionality
// super.doGet(req, resp);
// make sure to sent the event source content type and encoding
resp.setContentType("text/event-stream");
resp.setCharacterEncoding("UTF-8");
// start long running task
// NOTE: this uses threads to spawn tasks which is technically not
// allowed by the servlet spec, but it helps to demonstrate the
// point.
LongTask task = new LongTask(10);
Future<Boolean> result = executor.submit(task);
// wait until task is complete sending updates as events
int last = task.getCurrent();
while (!result.isDone()) {
if (task.getCurrent() != last) {
last = task.getCurrent();
// send the JSON-encoded data
StringBuilder data = new StringBuilder(128);
data.append("{\"current\":").append(last).append(",")
.append("\"total\":").append(task.getTotal()).append("}\n\n");
System.out.println("DATA: " + data);
writeEvent(resp, "status", data.toString());
}
}
// send finalized response
writeEvent(resp, "status", "{\"complete\":true}");
}
/**
* Write a single server-sent event to the response stream for the given
* event and message.
*
* @param resp The response to write to
* @param event The event to write
* @param message The message data to include
*
* @throws IOException If an I/O error occurs
*/
protected void writeEvent(HttpServletResponse resp,
String event, String message)
throws IOException {
// get the writer to send text responses
PrintWriter writer = resp.getWriter();
// write the event type (make sure to include the double newline)
writer.write("event: " + event + "\n\n");
// write the actual data
// this could be simple text or could be JSON-encoded text that the
// client then decodes
writer.write("data: " + message + "\n\n");
// flush the buffers to make sure the container sends the bytes
writer.flush();
resp.flushBuffer();
}
/**
* This resembles a task that could be used to perform some long running
* task that executes in a separate thread. This example just sleeps
* over and over sending its progress.
*/
public static class LongTask implements Callable<Boolean> {
private int current;
private int total;
public LongTask(int total) {
this.total = total;
}
public int getCurrent() {
return this.current;
}
public int getTotal() {
return this.total;
}
@Override
public Boolean call() throws Exception {
// do stuff (for now just sleep as an example)
while (++this.current < this.total) {
try { Thread.sleep(1000); }
catch (Exception e) { /* ignore */ }
}
return Boolean.TRUE;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment