This post is about using Nord Pool push notifications to receive urgent market messages, UMMs, in a Java application. It is mainly of interest for people in the energy sector, but could also be useful to someone looking to receive notifications using SignalR in a Java environment.
Nord Pool
Nord Pool is a European market for trading power (electricity), owned by Nasdaq.
Urgent Market Messages
An urgent market message, or UMM, is used in the energy sector to inform the market about planned and unplanned events that affect the available power in production, consumption or transmission units. For example, information about a planned maintenance of a nuclear power plant would be sent as a UMM, as would information about a power line connecting two countries being cut by mistake.
Nord Pool aggregates UMMs from European power companies and provide a REST API to get UMMs. They also provide push notifications to asynchronously receive real-time UMM information using SignalR.
SignalR
SignalR is a Microsoft library for sending asynchronous notifications from servers to clients using standard web protocols.
There are two versions of SignalR that are not compatible: ASP.NET SignalR and ASP.NET Core SignalR. The Nord Pool push notifications use the older ASP.NET SignalR version, so it is important to use a client that supports that version.
In this case, we’re looking for a Java client, and luckily there is one available on GitHub. The readme for this project marks it as obsolete, and points to a version that supports the newer ASP.NET Core SignalR. However, since we want to connect to Nord Pool we will ignore the warning and use the old version.
There does not seem to be any version of the Java client uploaded to a Maven repository, so the first step is to clone the Git repository and build the code locally. There are Gradle build scripts available; I did not have Gradle available where I’m currently working, so I added simple POM files and built using Maven. The only module that is required for what we are doing here is signalr-client-sdk
.
Listening for Notifications Using the Java Client
Let’s develop a Java domain service that listens for UMM notifications and calls registered handlers when UMM events occur. We start by defining the service interface that allows you to register new handlers, and start listening:
public interface UmmNotificationService { void registerNotificationHandler(UmmNotificationHandler notificationHandler); void startListeningForNotifications(); }
The Nord Pool push notification documentation describes three types of events:
- New message
- Update message
- Cancel / dismiss message
We therefore define a notification handler interface with three methods corresponding to the events:
public interface UmmNotificationHandler { void onNewMessage(String jsonUmm); void onUpdateMessage(String jsonUmm); void onDismissMessage(String jsonUmm); }
Note that the methods are defined to take strings with the JSON representation of the UMMs. This is in order to make the example simpler; in a real system you would normally create a Umm
entity and a factory class UmmFactory
to create Umm
objects from their JSON representation.
We can now create an empty implementation of the service interface, so that we can start writing an integration test (note that we anticipate that we will need to define which URL to connect to):
public class NordpoolUmmNotificationService implements UmmNotificationService { public NordpoolUmmNotificationService(String ummPushUrl) { } @Override public void registerNotificationHandler(UmmNotificationHandler notificationHandler) { } @Override public void startListeningForNotifications() { } }
We are now in a position where we can create an integration test for our new service. Here I have to admit that I don’t really know how to create a solid, reliable and repeatable test. At the moment, the best I know how to do is to connect to Nord Pool, wait until a UMM push notification arrives and then verify that what we received was a correct UMM:
public class NordpoolUmmNotificationServiceIT { @Test public void receiveNotifications() throws Exception { NordpoolUmmNotificationService service = new NordpoolUmmNotificationService("https://ummws.nordpoolgroup.com"); TestUmmNotificationHandler notificationHandler = new TestUmmNotificationHandler(); service.registerNotificationHandler(notificationHandler); service.startListeningForNotifications(); notificationHandler.latch.await(); String umm = notificationHandler.umms.get(0); // Parse JSON and verify that it really is a UMM. } private static final class TestUmmNotificationHandler implements UmmNotificationHandler { private CountDownLatch latch = new CountDownLatch(1); private List<String> umms= new ArrayList<>(); @Override public void onNewMessage(String umm) { umms.add(umm); latch.countDown(); } @Override public void onUpdateMessage(String umm) { umms.add(umm); latch.countDown(); } @Override public void onDismissMessage(String umm) { umms.add(umm); latch.countDown(); } } }
Note that new UMMs are produced only a few per hour, so the above test may take a very long time to run.
After some trial and error, the following implementation turns out to make the test pass:
import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.JsonElement; import microsoft.aspnet.signalr.client.LogLevel; import microsoft.aspnet.signalr.client.hubs.HubConnection; import microsoft.aspnet.signalr.client.hubs.HubProxy; import microsoft.aspnet.signalr.client.transport.ClientTransport; import microsoft.aspnet.signalr.client.transport.ServerSentEventsTransport; public class NordpoolUmmNotificationService implements UmmNotificationService { private static final Logger LOG = LoggerFactory.getLogger(NordpoolUmmNotificationService.class); private String ummPushUrl; private Set<UmmNotificationHandler> notificationHandlers = new CopyOnWriteArraySet<>(); public NordpoolUmmNotificationService(String ummPushUrl) { LOG.info("Creating new NordpoolUmmNotificationService: ummPushUrl={}", ummPushUrl); this.ummPushUrl = ummPushUrl; } @Override public void registerNotificationHandler(UmmNotificationHandler notificationHandler) { LOG.info("Registering a new notification handler: {}", notificationHandler); notificationHandlers.add(notificationHandler); } @Override public void startListeningForNotifications() { LOG.info("Start listening for notifications"); microsoft.aspnet.signalr.client.Logger logger = new Slf4jLogger(LOG); HubConnection connection = new HubConnection(ummPushUrl, "", true, logger); ClientTransport clientTransport = new ServerSentEventsTransport(logger); HubProxy proxy = connection.createHubProxy("MessageHub"); proxy.on("newMessage", data -> { onNewMessage(data.toString()); }, JsonElement.class); proxy.on("updateMessage", data -> { onUpdateMessage(data.toString()); }, JsonElement.class); proxy.on("dismissMessage", data -> { onDismissMessage(data.toString()); }, JsonElement.class); connection.start(clientTransport); } private void onNewMessage(String umm) { for (UmmNotificationHandler notificationHandler : notificationHandlers) { LOG.debug("Calling onNewMessage on notification handler: umm={}, notificationHandler={}", umm, notificationHandler); notificationHandler.onNewMessage(umm); } } private void onUpdateMessage(String umm) { for (UmmNotificationHandler notificationHandler : notificationHandlers) { LOG.debug("Calling onUpdateMessage on notification handler: umm={}, notificationHandler={}", umm, notificationHandler); notificationHandler.onUpdateMessage(umm); } } private void onDismissMessage(String umm) { for (UmmNotificationHandler notificationHandler : notificationHandlers) { LOG.debug("Calling onDismissMessage on notification handler: umm={}, notificationHandler={}", umm, notificationHandler); notificationHandler.onDismissMessage(umm); } } /** * An adapter class that takes an Slf4j logger and turns it into a * <code>microsoft.aspnet.signalr.client.Logger</code>. */ private static final class Slf4jLogger implements microsoft.aspnet.signalr.client.Logger { private final Logger slf4jLogger; Slf4jLogger(Logger slf4jLogger) { this.slf4jLogger = slf4jLogger; } @Override public void log(String message, LogLevel level) { switch (level) { case Critical: slf4jLogger.error(message); break; case Information: slf4jLogger.info(message); break; case Verbose: slf4jLogger.debug(message); break; default: throw new IllegalStateException("Unknonwn enum constant: " + level); } } } }
A few notes regarding the code above:
- The SignalR Java client adds
/signalr
to the URL you connect to, so in the case of Nord Pool you use the URLhttps://ummws.nordpoolgroup.com
to connect. - The name used in the call
connection.createHubProxy
must be"MessageHub"
, otherwise you receive HTTP 500, Interval Server Error, from Nord Pool. - The default client transport will use a
WebsocketTransport
. I have not got this to work, possibly because we are behind an HTTP proxy. TheServerSentEventsTransport
does work, however.