Reporting Gradle Builds using WebSockets

If you have a build server you might want to receive reporting from your build. Many build bots offer this kind of reporting, but I decided to implement it myself in a standard Gradle build script. I override the default logger and replace it with one that writes all of the logging to a web socket. I have also created a very simple Java EE service which can consume logging messages and rebroadcast them to a different web socket using JMS¹.

Gradle Configuration

buildscript {
    dependencies {
        /* I'm using the tyrus libraries for my web socket client.
         Because logging is part of the build and not the project, 
         they must be declared classpath and in the buildscript.dependencies
         stanza.    
        */
        classpath 'org.glassfish.tyrus:tyrus-client:1.+'
        classpath 'org.glassfish.tyrus:tyrus-server:1.+'
        classpath 'org.glassfish.tyrus:tyrus-container-grizzly:1.+'
    }
}
//Now we begin the setup for the WebSocket Logger
import org.glassfish.tyrus.client.*;

gradle.useLogger(new WebSocketLogger());

class WebSocketLogger implements org.gradle.api.logging.StandardOutputListener {

    def manager = ClientManager.createClient();
    def session = manager.connectToServer(WebSocketLoggerClientEndpoint.class, java.net.URI.create("ws://localhost:8080/log_viewer/logging"));

    // useLogger replaces the default logging.  I am writing to a tmp file for debugging purposes.
    def tmp = new File('/tmp/log.txt');

    @Override
    void onOutput(CharSequence charSequence) {
        tmp.append(charSequence +"\n");
        session.basicRemote.sendText(charSequence.toString());
    }

    @javax.websocket.ClientEndpoint
    class WebSocketLoggerClientEndpoint {

        @javax.websocket.OnMessage
        public void processMessageFromServer(String message, javax.websocket.Session session) {
            tmp.append(message +"\n");
        }

        @javax.websocket.OnError
        public void handleError(javax.websocket.Session session, Throwable thr) {
            tmp.append('Err' + thr.message +"\n");
        }
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

Java EE Server

The server side was more complex because of how CDI and WebSockets interact in Java EE 7. The code is really simple and benefits much more from browsing in GitHub than in snippets here. You may view the server source here : https://github.com/secondsun/log_viewer².

All this code does is take messages sent to the socket found at “ws://localhost:8080/log_viewer/logging” and rebroadcasts them to “ws://localhost:8080/log_viewer/read”

Conclusion

Being able to rebroadcast log messages is neat and useful. Additionally having a working example for connecting websockets to JMS was a lot of fun to put together.

Foot notes

1: I would have used CDI events, but CDI and the ServerEndpoint annotation do not get along. There are several JIRAs tracking this issue.
* WEBSOCKET_SPEC-196
* JMS_SPEC-121
* CDI-370
2: Thanks to https://blogs.oracle.com/brunoborges/entry/integrating_websockets_and_jms_with for help with getting this working.

A quick rant on Android Studio

As a software developer I love for things to be simple, and I hate for things to get between me, my code, and execution. As such I like powerful, scriptable build systems with wonderful dependency management (Gradle & make much love). I love for my coding tools to be able to use to be able to look at my build scripts and do intelligent things (see NetBeans with Maven, seriously this is done very, very right). I don’t like it when my coding tools try to subvert me (Eclipse is a war crime in this regard) or make changes on my behalf.

Android Studio, which is based on IntelliJ is starting toward the “subvert me” route by pestering me to use its instance of the Android SDK instead of my own. This is important because my instance of the SDK includes all of the packages downloaded already and it’s the location all of my build tools reference. Now when Android Studio overrides my choices it points my projects to a broken/unconfigured SDK, and suddenly things which worked hours before now have errors cropping up. Even worse, if I change my projects file back, Studio begins pestering me to use its SDK again, and it will every time you open your project until you use its SDK and start this whole mess over again.

I guess my point is, if your project breaks check your local.project file and make sure that it is using your SDK and not Android Studio’s.

Realtime sync with AeroGear on Android

AeroGear 1.2.0 included support for AeroGear Unified Push. Part of the technology underpinning that feature on Android are the Registrar APIs. These APIs make it very easy to create a message generating or message consuming Object and integrate it with your applications in Android. In my own time I have experimented with a MQTT PushRegistrar and a WebSocket PushRegistrar. Using the my WebSocket Push Registrar I created a very simple text sync demo.

As a quick bit of background, I also wrote the server component of the demo. It runs on vert.x and persists data to MongoDB. I implemented Neil Fraser’s Differential Synchronization process on both the client and server side. This algorithm handles merging the documents various states across devices. It is also very well written and easy to follow.

In this demo a user loads a document (using a RESTful Pipe). In the callback, the Activity registers itself as a MessageHandler with the Registrar API, creates a configuration object which describes a WebSocketPushRegisrar and passes this configuration into the Registrations class which creates a new Web Socket and connects it to the server. Now when the document is changed by a different application, a diff is received on the WebSocket and its contents are given to the Activity. If the Activity edits the document it creates a diff and saves it to the server. The server is in charge of synchronizing diffs across the different devices.

CRUD operations are performed using two Pipes. A Pad Pipe which is responsible for creating documents, getting lists of documents, and getting an individual document. The second Pipe is a PadDiff Pipe. Its job is to send diffs to the server.

[source lang=”java”]
//Create a Pad Pipe
PipeConfig padConfig = new PipeConfig(URL, Pad.class);
pipeline.pipe(Pad.class, padConfig);

//Use the Pad Pipe
//By passing in the padListFragment to Pipeline#get we are telling Aerogear to
//wrap the Pipe in a Loader.

public void getPads(GetPadsListFragment getPadsListFragment, PadCallback padCallback) {
pipeline.get("pad", getPadsListFragment, this).read(padCallback);
}

//Create a PadDiff pipe. PadDiff pipes are 1:1 with the "Session" which is a identifier for the web socket.
//In this app a session is killed with the fragment is put into the background. Thus I decided
//to make a PadDiff Pipe 1:1 with the fragment.
public Pipe<PadDiff> padDiff(PadFragment padFragment, String sessionId) {
PipeConfig config = new PipeConfig(URL, PadDiff.class);
config.setName("padDiff");
config.setEndpoint("/padDiff/" + sessionId);
pipeline.pipe(PadDiff.class, config);
return pipeline.get("padDiff", padFragment, this);
}
[/source]

From the devices POV the documents are fetched using the following code:

[source lang=”java”]
ReadFilter filter = new ReadFilter();
//Currently the way to get an element by ID is to use the LinkUri.
//This is scheduled to be fixed in a future version of Aerogear
filter.setLinkUri(URI.create("/" + padId));
pipeline.get("pad", fragment, this).read(filter, new PadCallback(padId));
[/source]

In `PadCallback` the Pad is loaded and a WebSocket is created. When the websocket creation is finished, the server sends a message to the Activity with a session_id object. This message is received on the onMessage method of the Activity. This method is also responsible for handling error messages and handling diff messages which are updates from other clients.

First we handle the response from getting the pad:

[source lang=”java”]
//Called from PadCallback#onSuccess
public void handlePads(List<Pad> pads) {
pad = pads.get(0);
setupEditor(pad);
PushConfig pushConfig = new PushConfig();
pushConfig.setPushServerURI(URI.create("ws://10.0.2.2:8080/pad/" + pad.getId()));

//This will open the web socket.
((AeropadApplication) getActivity().getApplication()).register(pushConfig);
}
[/source]

Then the Activity receives a callback from the WSPushRegistrar with a session id. The server will send different messages depending on what information it is sending to the client. In AeroGear a MessageHandler(in this case our Activity) receives a message from the WSPushRegisrar and extracts the content based on if there is a “diff”, “session_id”, or “error” object in the response. These keys are specific to this server but the general principle can be applied to any application.

[source lang=”java”]
@Override
public void onMessage(Context context, Bundle message) {
try {
String json = message.getString(WebsocketMessageReceiver.PAYLOAD);
JSONObject object = new JSONObject(json);
if (object.has("diff")) {
handleDiff(object);
} else if (object.has("session_id")) {
//Create a PadDiff Pipe
diffPipe = ((AeropadApplication)getActivity().getApplication()).padDiff(this, object.getString("session_id"));
} else if (object.has("error")) {
Log.e("ERROR", json);
Toast.makeText(getActivity(), json, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), json, Toast.LENGTH_LONG).show();
}
} catch (JSONException e) {
Log.e("MESSAGE", e.getMessage(), e);
}
}
[/source]

The full source code for the Android application can be found at my Github. It is build using the aar branch on the official AeroGear Android repository.

Devnexus Talk: Consuming RESTful services with Android

Last week I gave a talk at Devnexus about consuming RESTful services on Android. It was a really great experience and I did learn a few things.

They way I learned things was by reading the Android docs, the Android source, and writing demo code. Some of the interesting bits I learned were:

  • AsyncTask won’t always work in a Service method. It will work in onCreate but not many places else.
  • In general, don’t use AsyncTask.
  • If you are performing a long operation (networking for example), use a Loader or a Service.
  • Look at Googles iosched app for examples on how to do ANYTHING in Android. They put a ton of crap into it.

Here are the links to my slides and examples for general web consumption.