The Java ServerEventClient
is an idiomatic port of ServiceStack's
C# Server Events Client to Java providing a productive
client to consume ServiceStack's real-time Server Events that can be used in any
Java/JVM (JRE 7+) Client/Server Applications or Java/Kotlin Android applications.
Install​
The AndroidServerEventsClient
for Android is available in the
net.servicestack:android package which
can be installed in your
build.gradle
with:
dependencies {
implementation 'net.servicestack:android:1.1.0'
...
}
Or in Maven with:
<dependency>
<groupId>net.servicestack</groupId>
<artifactId>android</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>
Other Java/JVM languages running on the JVM (JRE 7+) can use the ServerEventsClient
in the
net.servicestack:client package which can
be installed using Gradle:
compile 'net.servicestack:client:1.0.48'
Or Maven:
<dependency>
<groupId>net.servicestack</groupId>
<artifactId>client</artifactId>
<version>1.0.48</version>
<type>pom</type>
</dependency>
The AndroidServerEventsClient
class for Android inherits ServerEventsClient
to provide enhanced
functionality like alternative non-blocking APIs for all Sync APIs using Android Async Tasks. It also requires
the use of an external OkHttp Client dependency since the
HttpURLConnection
implementation in Android doesn't support cancellable non-blocking requests on HTTP Streams. Otherwise both implementations provide the same functionality and are interchangeable, for the
purposes of demonstration we'll be using AndroidServerEventsClient
.
To configure Server Sent Events on the client create a new instance of AndroidServerEventsClient
with the
baseUrl and the channels you want to connect to, e.g:
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.setOnConnect(sub -> { // Successful SSE connection
Log.d("You've connected! welcome " + sub.getDisplayName());
})
.setOnJoin(e -> { // User has joined subscribed channel
Log.d("Welcome, " + e.getDisplayName());
})
.setOnLeave(e -> { // User has left subscribed channel
Log.d(e.getDisplayName() + " has left the building");
})
.setOnUpdate(e -> { // User channel subscription was changed
Log.d(e.getDisplayName() + " has left the building");
})
.setOnMessage(msg -> { }) // Invoked for each other message
//... Register custom handlers
.registerHandler("chat", (client, e) -> { // Invoked for cmd.chat adhoc messages
ChatMessage chatMsg = JsonUtils.fromJson(e.getJson(), ChatMessage.class);
})
.registerReceiver(MyReceiver.class) // Register Global 'cmd.' default receiver
.registerNamedReceiver("tv",TvReceiver.class) // Register named 'tv.' receiver
.addListener("theEvent", msg -> {}) // Add listener for pub/sub event trigger
.setOnException(e -> { }) // Invoked on each Error
.setOnReconnect(() -> { }) // Invoked after each auto-reconnect
.start(); // Start listening for Server Events!
//Global Receiver Class
public class MyReceiver extends ServerEventReceiver {
public void announce(String message){} // Handle messages with simple argument
public void chat(ChatMessage message){} // Handle messages with complex type argument
public void customType(CustomType message){} // Handle complex types with default selector
@Override // Handle other unknown messages
public void noSuchMethod(String selector, Object message){}
}
//Named Receiver Class
public class TvReciever extends ServerEventReceiver {
public void watch(String videoUrl){} // Handle 'tv.watch {url}' messages
public void off(){} // Handle 'tv.off' messages
}
Message Events​
ServiceStack Server Events has 4 built-in events sent during a subscriptions life-cycle:
- onConnect - sent when successfully connected, includes the subscriptions private
subscriptionId
as well as heartbeat and unregister urls that's used to automatically setup periodic heartbeats - onJoin - sent when a new user joins the channel
- onLeave - sent when a user leaves the channel
- onUpdate - sent when a user's channels subscription was updated
The onJoin/onLeave/onUpdate events can be turned off with
ServerEventsFeature.NotifyChannelOfSubscriptions=false
.
All other messages can be handled with the catch-all:
- onMessage - fired when any other message is sent
Server Event Client Events​
Other top-level events the ServerEventClient
fires that can be handled include:
- onException - Invoked on each error the client receives
- onReconnect - Invoked after each time the client had to auto-reconnect
Selectors​
A selector is a string that identifies what should handle the message, it's used by the client to route the message to different handlers. The client bindings supports 4 different handlers out of the box:
Global Event Handlers​
The easiest way to handle a custom event is to define a handler, e.g:
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.registerHandler("paint", (client, e) -> {
String color = JsonUtils.fromJson(e.getJson(), String.class);
Log.d("Painting the " + e.getCssSelector() + " " + color);
})
.registerHandler("chat", (client, e) -> {
ChatMessage chatMsg = JsonUtils.fromJson(e.getJson(), ChatMessage.class);
Log.d("Received " + chatMsg.getMessage() + " from " + chatMsg.getFromName());
})
.start();
The selector to invoke a global event handler is:
cmd.{handler} {message}
Which can be sent in ServiceStack with:
ServerEvents.NotifyChannel("home", "cmd.paint$#town", "red");
ServerEvents.NotifyChannel("home", "cmd.chat", new ChatMessage { ... });
Where {handler}
is the name of the handler you want to invoke, e.g cmd.paint
. The first argument is
the ServerEventsClient
instance whilst the 2nd argument a structured ServerEventMessage
which for
the above Server Event is populated with:
.registerHandler("paint", (client, e) -> {
e.getChannel() //= home
e.getData() //= home@cmd.paint$#town "red"
e.getSelector() //= cmd.paint
e.getJson() //= "red"
e.getOp() //= cmd
e.getTarget() //= paint
e.getCssSelector() //= #town
})
The message body is serialized as JSON and accessible from e.getJson()
and can be extracted using JsonUtils
,e.g:
String color = JsonUtils.fromJson(e.getJson(), String.class); //Simple string message body
ChatMessage chatMsg = JsonUtils.fromJson(e.getJson(), ChatMessage.class); //Complex Type body
Postfix CSS selector​
All server event handler options also support a postfix CSS selector for specifying what each handler should be bound to with a $
followed by the CSS selector, e.g:
cmd.{handler}${cssSelector} {value}
A concrete example for calling the above API would be:
cmd.paint$#town red
INFO
Spaces in CSS selectors need to be encoded with %20
Handling Messages with the Default Selector​
All IServerEvents
Notify API's includes overloads for sending messages without a selector that by convention will take the format cmd.{TypeName}
.
As they're prefixed with cmd.*
these events can be handled with either a handler (as above) or a global receiver based on Message type name, e.g:
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.registerReceiver(MyGlobalReceiver.class)
.start();
public class TestGlobalReceiver extends ServerEventReceiver {
public void setterType(SetterType value) {
}
public void customType(CustomType request) {
}
}
Which will be called when messages are sent without a selector, e.g:
public class MyServices : Service
{
public IServerEvents ServerEvents { get; set; }
public void Any(Request request)
{
ServerEvents.NotifyChannel("home", new CustomType { ... });
ServerEvents.NotifyChannel("home", new SetterType { ... });
}
}
Whilst Named Receivers are used to handle messages sent to a specific namespaced selector, registering a
Global Receiver allows you to handle messages sent with the cmd.*
selector which is also the default
selector used when sending messages with no selector.
Receivers​
In programming languages based on message-passing like Smalltalk and Objective-C invoking a method is done by sending a message to a receiver. This is conceptually equivalent to invoking a method on an instance in C# where both these statements are roughly equivalent:
// Objective-C
[receiver method:argument]
// C#
receiver.method(argument)
Support for receivers is available in the following format:
{receiver}.{target} {msg}
Registering Receivers​
Registering a receiver can be done with a map of the object instance and the name you want it to be exported as. E.g. we can add a "css" receiver to handle with:
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.registerNamedReceiver("css", CssReceiver.class)
.setResolver(new MyResolver(mainActivity))
.start();
public class CssReceiver extends ServerEventReceiver {
private MainActivity parentActivity;
public CssReceiver(MainActivity parentActivity) {
this.parentActivity = parentActivity;
}
public void backgroundImage(String message){
String url = message.startsWith("url(")
? message.substring(4, message.length() - 1)
: message;
App.get().readBitmap(url, bitmap -> {
ImageView chatBackground = (ImageView)parentActivity.findViewById(R.id.chat_background);
parentActivity.runOnUiThread(() -> chatBackground.setImageBitmap(bitmap));
});
}
public void background(String message){
String color = message.replace("#", "#AA");
String cssSelector = super.getRequest().getCssSelector();
parentActivity.runOnUiThread(() -> {
if (Objects.equals(cssSelector, "#top")){
parentActivity.getSupportActionBar().setBackgroundDrawable(
new ColorDrawable(colorVal)
);
}
});
}
}
public class MyResolver implements IResolver {
private MainActivity parentActivity;
public MyResolver(MainActivity parentActivity) {
this.parentActivity = parentActivity;
}
@Override
public Object TryResolve(Class cls){
if (cls == CssReceiver.class){
return new CssReceiver(this.parentActivity);
}
return cls.newInstance();
}
}
Which will invoke backgroundImage
method off a new instance of the CssReceiver
class that's triggered with:
css.background-image url(https://bit.ly/1yIJOBH)
and can be sent to all subscriptions on the home channel in ServiceStack with:
ServerEvents.NotifyChannel("home", "css.background-image", "url(https://bit.ly/1yIJOBH)");
This works the same with Global Receivers.
Inheriting ServerEventReceiver​
By inheriting ServerEventReceiver
:
class ServerEventReceiver implements IReceiver {
public client: ServerEventsClient;
public request: ServerEventMessage;
noSuchMethod(selector: string, message:any) {}
}
Receivers can access additional built-in functionality where it will allow receivers to access the
ServerEventsClient
client dependency, the ServerEventMessage
that was received, it also lets you handle
any unhandled messages sent by implementing noSuchMethod()
, e.g:
class JavaScriptReceiver extends ServerEventReceiver {
public void chat(ChatMessage chatMessage){
LogMessage request = new LogMessage()
.setChannel(msg.getChannel())
.setMessage(msg.getMessage());
super.client.getServiceClient().post(request);
}
public void announce(String message){
Toast.makeText(this.parentActivity, message, Toast.LENGTH_LONG);
}
public void toggle(){
if ("#sidebar".equals(super.request.getCssSelector())){
LinearLayout sidebarLayout=(LinearLayout)this.findViewById(R.id.sidebarLayout);
sidebarLayout.setVisibility(sidebarLayout.getVisibility() == LinearLayout.INVISIBLE
? LinearLayout.VISIBLE
: LinearLayout.INVISIBLE
);
}
}
public void noSuchMethod(String selector, Object message){
ServerEventMessage msg = (ServerEventMessage)message;
Log.d("Unhandled " + selector + " was sent message: " + msg.getJson());
}
}
client.registerReceiver(JavaScriptReceiver.class); //register Global Receiver
These can triggered with:
ServerEvents.NotifyChannel(channel, new ChatMessage { ... });
ServerEvents.NotifyChannel(channel, "cmd.announce", "Hello, World!");
ServerEvents.NotifyChannel(channel, "cmd.toggle$#sidebar");
ServerEvents.NotifyChannel(channel, "cmd.UnknownSelector", new Message { ... });
Dependency Resolvers​
You can control the lifetime of the receivers by injecting a custom resolver which will let you reuse the same
Receiver instance by using a SingletonInstanceResolver
, e.g:
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.setResolver(new SingletonInstanceResolver())
.registerReceiver(JavaScriptReceiver.class)
.start();
Which is implemented with:
public class SingletonInstanceResolver implements IResolver {
ConcurrentMap<Class, Object> cache = new ConcurrentHashMap<>();
@Override
public Object TryResolve(Class cls) {
Object instance = cache.get(cls);
if (instance == null){
try {
Object newInstance = cls.newInstance();
instance = (instance = cache.putIfAbsent(cls, newInstance)) == null
? newInstance
: instance;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return instance;
}
}
Custom Resolvers are also useful configuring the dependencies in your Receiver classes, e.g. we use this
above to inject our MainActivity
into our CssReceiver
class.
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.setResolver(new MyResolver())
.registerReceiver(CssReceiver.class)
.start();
public class MyResolver implements IResolver {
private MainActivity parentActivity;
public MyResolver(MainActivity parentActivity) {
this.parentActivity = parentActivity;
}
@Override
public Object TryResolve(Class cls){
if (cls == CssReceiver.class){
return new CssReceiver(this.parentActivity);
}
return cls.newInstance();
}
}
Event Triggers​
Triggers enable a pub/sub event model where multiple listeners can subscribe and be notified of an event.
Registering an event handler can be done at anytime using the addListener()
API, e.g:
Action<ServerEventMessage> handler = e -> {
Log.d("received event " + e.getTarget() + " with arg: " + e.getJson());
};
ServerEventsClient client = new AndroidServerEventsClient(baseUrl, "home")
.addListener("customEvent", handler)
.start();
//Register another listener to 'customEvent' event
List<ServerEventMessage> msgs1 = new ArrayList<>();
client.addListener("customEvent", msgs1::add);
The selector to trigger this custom event is:
trigger.customEvent arg
trigger.customEvent {json}
Which can be sent in ServiceStack with a simple or complex type argument, e.g:
ServerEvents.NotifyChannel(channel, "trigger.customEvent", "arg");
ServerEvents.NotifyChannel(channel, "trigger.customEvent", new ChatMessage { ... });
Removing Listeners​
Use removeListener()
to stop listening for an event, e.g:
//Remove first event listener
client.removeListener("customEvent", handler);
Channel Subscriber APIs​
You can use any of the APIs below to update an active Subscriptions Channels:
client.subscribeToChannels("chan3","chan4");
client.unsubscribeFromChannels("chan1","chan2");
//Alternatively subscribe/unsubscribe to channels in the same request with:
UpdateEventSubscriber request = new UpdateEventSubscriber()
.setSubscribeChannels(Func.toList("chan3","chan4"))
.setUnsubscribeChannels(Func.toList("chan1","chan2"));
client.updateSubscriber(request);
All Service Client APIs in AndroidServiceClient
also have non-blocking versions with an an Async
suffix
that utilize Android's Async Task and optional callbacks for performing non-blocking Service Client requests, e.g:
client.updateSubscriberAsync(request, () -> {
Log.d("SUCCESS!");
}, e -> {
Log.d("FAILED: " + e.toString());
});
Get Channel Subscribers​
Once connected, you can get a list of channel subscribers the ServerEventsClient
is currently connected
to with:
List<ServerEventUser> channelUsers = client.getChannelSubscribers();
Func.each(channelUsers, user -> {
Log.d(user.getUserId() + " @" + user.getDisplayName() + " " + user.getProfileUrl());
});
Accessing ServiceClient​
Alternatively you can access the channel subscribers using the built-in JsonServiceClient
, e.g:
client.getServiceClient().get(new GetEventSubscribers()
.setChannels(Func.toList("chan1","chan2")));
Which you can also use to call your own Services:
client.getServiceClient().post(new MyRequest());
Integration Test Examples​
More examples of ServerEventClient
usage can be found in the Test Suites below:
Java ServerEvents Examples​
Android Java Chat​
Java Chat client utilizing Server Events for real-time notifications and enabling seamless OAuth Sign In's using Facebook, Twitter and Google's native SDKs: