<img height="1" width="1" style="display:none;" alt="" src="https://px.ads.linkedin.com/collect/?pid=299788&amp;fmt=gif">

Spring and HipChat Sitting in a Tree – Part 5

Software Development, Software Solutions

By Joel Brinkman

Tens of thousands of loyal readers who have followed this series and tried each and every post have commented on a few very large holes in the Spring Integration code used to illustrate a possible architecture for HipChat plugins. I'll attempt to patch a few of these holes in this final post of the series. At the end, I'll also point out the remaining problems that I've glossed over or have thus far completely ignored.

The last post got us to the point where our HipChat plugin could be installed and any message posted to a room named "WhackyChat" would be echoed back to the room. While this incredible and impressive behavior surely brought tears to your eyes, there are a few practical problems that remain with our sample application.

Unfortunately, all of you who tried running this little plugin for more than an hour or so noticed that once the access token expires, we're no longer able to post messages back to the room. Up to this point, I was simply too lazy to deal with this clearly fatal flaw. We're going to make a few changes to our flow such that when an API failure happens during our flow, an error message will be placed on the implicit errorChannel that will be handled by a chain that is quite similar to the chain we designed previously for fetching the access token. In fact, we're no longer going to fetch the access token as part of the normal flow - we're only going to fetch it after an API error! In order to make this approach work, we need to enable message history and each channel must be restartable given a failed message. In other words, whatever the state of a message at the time of failure, that message must be valid when dumped back on the channel where the failure occurred.

When we're done today, your flow will look like this:
Before we do anything, first update Spring Boot to 1.1.9-RELEASE. Then, to solve the issue of a Tenant signing up after they had previously uninstalled your plugin, we are going to check for the existence of the Tenant in our type converter; we will check the database to see if the Tenant already exists. Remove the HipChatInstallationToTenantConverter and replace it with this ConverterFactory:
package com.isostech.swearjar.converter;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import com.isostech.swearjar.domain.Tenant;
import com.isostech.swearjar.dto.HipChatInstallation;
import com.isostech.swearjar.repository.TenantRepository;
public class HipChatInstallationToTenantConverterFactory implements ConverterFactory&lt;HipChatInstallation, Tenant&gt;, ApplicationContextAware {
private ApplicationContext applicationContext;
private final class HipChatInstallationToTenantConverter&lt;S extends HipChatInstallation, T extends Tenant&gt; implements Converter&lt;S, T&gt; {
private TenantRepository tenantRepository;
public HipChatInstallationToTenantConverter(Class&lt;T&gt; entityType, TenantRepository tenantRepository) {
this.tenantRepository = tenantRepository;
public T convert(HipChatInstallation dto) {
HipChatInstallationToTenantConverterFactory factory = HipChatInstallationToTenantConverterFactory.this;
T tenant = (T) tenantRepository.findByGroupId(dto.getGroupId());
tenant = (T) (tenant == null ? new Tenant() : tenant);
return tenant;
public &lt;T extends com.isostech.swearjar.domain.Tenant&gt; Converter&lt;HipChatInstallation, T&gt; getConverter(Class&lt;T&gt; targetType) {
// we are unable to autowire the repository, so we fetch it here...sad but true.
return new HipChatInstallationToTenantConverter&lt;HipChatInstallation, T&gt;(targetType, applicationContext.getBean(TenantRepository.class));
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
This will change our plugin such that it is no longer assumed each Tenant signing up is new. No biggie, but important.
Now, any channel that may experience an authorization error from HipChat will be restartable with the message at the time of the error. With this in mind, we'll author a chain that takes all messages on errorChannel, assumes they are authorization errors, resolves them and then dumps the original message back on the channel that first experienced the error. We'll do this by enabling message history and then creating a chain that processes error messages.
With this principle and an error handler, we will be able to fill the largest gap in our previous implementation. In episode 4, upon the expiration of our access token, the HipChat plugin would simply cease to function. What might such an error handler look like? Check dis:
&lt;!-- This will be executed on error --&gt;
&lt;!-- The chain assumes a Tenant will be shoved in the header --&gt;
&lt;!-- It also assumes the failedMessage can be processed by the replyChannel --&gt;
&lt;int:chain id=&quot;access-token-chain&quot; input-channel=&quot;errorChannel&quot; &gt;
&lt;int:header-enricher id=&quot;header-enricher-foo&quot; default-overwrite=&quot;true&quot; &gt;
&lt;int:reply-channel expression=&quot;payload.failedMessage.headers['history'].get(payload.failedMessage.headers['history'].size()-1).name&quot; /&gt;&lt;!-- We're going to route the failed message back to the last channel --&gt;
&lt;int:header name=&quot;failedMessage&quot; expression=&quot;payload.failedMessage&quot; /&gt;&lt;!-- Stuff the failed message in the header. We need it later! --&gt;
&lt;int:header name=&quot;tenant&quot; expression=&quot;payload.failedMessage.headers['tenant']&quot; /&gt;
&lt;int:header name=&quot;Content-Type&quot; expression=&quot;'application/json'&quot; /&gt;
&lt;int:header name=&quot;Authorization&quot; expression=&quot;'Basic ' + new String(T(org.apache.commons.codec.binary.Base64).encodeBase64(new java.lang.String(payload.failedMessage.headers['tenant'].oauthId + ':' + payload.failedMessage.headers['tenant'].oauthSecret).bytes))&quot; /&gt;
&lt;int:transformer id=&quot;get-token-body&quot; expression=&quot;'{&amp;quot;grant_type&amp;quot;:&amp;quot;client_credentials&amp;quot;, &amp;quot;scope&amp;quot;:&amp;quot;send_notification send_message view_messages view_group admin_room&amp;quot;}'&quot; /&gt;
&lt;int-http:outbound-gateway id=&quot;get-token-body-gateway&quot; mapped-request-headers=&quot;Authorization,Content-Type&quot; http-method=&quot;POST&quot; url=&quot;https://api.hipchat.com/v2/oauth/token&quot; expected-response-type=&quot;java.lang.String&quot; /&gt;
&lt;int:json-to-object-transformer id=&quot;token-json-converter&quot; type=&quot;com.isostech.swearjar.dto.HipChatAccessToken&quot; /&gt;
&lt;int:service-activator id=&quot;save-access-token&quot; expression=&quot;@accessTokenRepository.save(payload)&quot; /&gt;
&lt;int:transformer expression=&quot;headers['failedMessage']&quot; /&gt;
&lt;int:header-enricher default-overwrite=&quot;true&quot; &gt;
&lt;int:reply-channel expression=&quot;headers['history'].get(headers['history'].size()-1).name&quot; /&gt;&lt;!-- We're going to route the failed message back to the last channel --&gt;
This little gem assumes all errors are authorization errors and attempts to resolve them by requesting a new access token. While this is clearly not something you'd want in production, it works for now.
As you can see above, we are now relying on message history to route failed messages back to the channel where the failure occurred. Enable message history like this:
&lt;int:message-history tracked-components=&quot;fetchRoomsChannel,sendMessageChannel,addWebhookChannel&quot;/&gt;
There are a number of small changes made to the flow in order to support this new error handling approach. Please pull the latest branch to see these changes in full. This plugin can be deployed, supports multiple tenants, and will run forever (more or less).
There are still a few problems that remain unsolved. Some of these issues make the current code base unsuitable for a production deployment. For example, it is simply unacceptable to assume all errors mean the access token has expired. Perhaps we'll address that in a future post.
Get the codez. https://bitbucket.org/jbrinkman/swearjar/branch/part5

TAGS: Software Development, Software Solutions

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Subscribe to Our Newsletter

Recent Blog Posts