Search code examples
javaspring-bootwebsocketjacksonspring-hateoas

Broadcast a HATEOAS object as a websocket message with a Content-Type other than application/json


I need to render the class below (among several other classes that extends RepresentationModel) both on a RESTController and as a broadcast message to a websocket.

@Getter
@JsonPropertyOrder(alphabetic=true)
@Setter
public class MessageWrapper extends RepresentationModel<MessageWrapper> {
    private Object message;
    private String simpleName;

    public MessageWrapper(Object message) {
        this.message = message;
        this.simpleName = message.getClass().getSimpleName();
    }
}

I render it to the response body using the code below:

@RequestMapping(
    headers = {
        "Content-Type=application/json",
        "X-Requested-With=XMLHttpRequest"
    },
    method = RequestMethod.POST,
    produces = "application/hal+json",
    value = "/thread/{threadId}/message")
public HttpEntity<MessageWrapper> post(...) {
    MessageWrapper wrapper = ...

    HttpEntity<MessageWrapper> entity =
        new ResponseEntity<HateoasWrapper>(wrapper, HttpStatus.OK);

    return entity;
}

And it produces the following truncated response:

HTTP/1.1 200 
...
Content-Type: application/hal+json

{
    "_links": {
        "previous": {
            "href": "/thread/1044/message/86"
        },
        "self": {
            "href": "/thread/1044/message/87"
        }
    },
    "message": {
        ...
    },
    "simpleName":"..."
}

Notice the Content-Type of the response and the _links object attribute above. I created a javascript handler for that Content-Type and that _links attribute is critical. Now on a separate event sometime later, I need to broadcast the exact same JSON String to a websocket. So I did this:

@Autowired
private SimpMessagingTemplate messagingTemplate;

public void broadcastToUser(StompPrincipal principal, MessageWrapper wrapper) {
    SimpMessageHeaderAccessor simpHeaderAccessor =
        SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);

    simpHeaderAccessor.setUser(principal);
    simpHeaderAccessor.setLeaveMutable(true);

    messagingTemplate.convertAndSendToUser(
        principal.getUsername(),
        "/broadcast",
        wrapper,
        simpHeaderAccessor.getMessageHeaders());
}

And it produces the following message:

a["MESSAGE
destination:/user/broadcast
content-type:application/json
subscription:sub-0
message-id:tluhntae-0
content-length:267

{
    "links": [
        {
            "rel": "previous",
            "href": "/thread/1044/message/86"
        },
        {
            "rel": "self",
            "href": "/thread/1044/message/87"
        }
    ],
    "message": {...},
    "simpleName": "..."
}

"]

Notice that it has the links attribute instead of _links. Also notice that instead of having an object value like the one above, it has an array of objects as value. I am suppose to use the same javascript callback to the RESTController response above but cannot since the links attribute has a different structure. I can marshall it on the javascript side but I rather not an instead attempted to configure it to render it as what I expected it to be rendered. I read that this is because I am rendering the websocket message as application/json instead of application/hal+json thats why it uses links instead of _links. So I set the Content-Type of the message as HAL before broadcasting it:

MimeType applicationJson;
        
if(RepresentationModel.class.isAssignableFrom(body.getClass())) {
    applicationJson = new MimeType("application", "hal+json");
} else {
    applicationJson = new MimeType("application", "json");
}

simpHeaderAccessor.setContentType(applicationJson);

It throws the following error though:

2020-10-26 23:40:40.799 ERROR 6200 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.messaging.converter.MessageConversionException: Unable to convert payload with type='MessageWrapper', contentType='application/hal+json', converter=[CompositeMessageConverter[converters=[org.springframework.messaging.converter.StringMessageConverter@66389c40, org.springframework.messaging.converter.ByteArrayMessageConverter@724493b5, org.springframework.messaging.converter.MappingJackson2MessageConverter@1228bacc]]]] with root cause

org.springframework.messaging.converter.MessageConversionException: Unable to convert payload with type='MessageWrapper', contentType='application/hal+json', converter=[CompositeMessageConverter[converters=[org.springframework.messaging.converter.StringMessageConverter@66389c40, org.springframework.messaging.converter.ByteArrayMessageConverter@724493b5, org.springframework.messaging.converter.MappingJackson2MessageConverter@1228bacc]]]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.doConvert(AbstractMessageSendingTemplate.java:187) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at org.springframework.messaging.core.AbstractMessageSendingTemplate.convertAndSend(AbstractMessageSendingTemplate.java:150) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at org.springframework.messaging.simp.SimpMessagingTemplate.convertAndSendToUser(SimpMessagingTemplate.java:230) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at org.springframework.messaging.simp.SimpMessagingTemplate.convertAndSendToUser(SimpMessagingTemplate.java:211) ~[spring-messaging-5.3.0-M1.jar:5.3.0-M1]
    at dev.gideon.chatapp.service.BroadcastService.broadcastToUser(BroadcastService.java:54) ~[classes/:na]
    ...

I really need to set the websocket message's Content-Type to HAL for it to be marshalled the way I expected it to be. Is there anyway to implement this? I am asking this question because aside from me not wanting to do separate javascript codes that handles the json object with the links attribute with an array value and the _links attribute with the object value, I also believe that it is indeed proper for it to be broadcasted with a HAL Content-Type so that other clients aside from my own will treat it as such and not just a normal JSON broadcasted message.


Solution

  • There are no default MessageConverter for websocket broadcast message of Content-Type application/hal+json. I need to implement my own MessageConverter for my custom MIME type. I created a converter that extends AbstractMessageConverter:

    public class HalMessageConverter extends AbstractMessageConverter {
        public HalMessageConverter() {
            MimeType hal = new MimeType("application", "hal+json");
            this.addSupportedMimeTypes(hal);
        }
    
        @Override
        protected boolean supports(Class<?> clazz) {
            return RepresentationModel.class.isAssignableFrom(clazz);
        }
    
        @Override
        protected Object convertToInternal(
            Object payload,
            MessageHeaders headers,
            Object conversionHint) {
    
            TypeConstrainedMappingJackson2HttpMessageConverter converter = 
                new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class);
    
            Module module = new Jackson2HalModule();
            LinkRelationProvider provider = new DefaultLinkRelationProvider();
    
            Jackson2HalModule.HalHandlerInstantiator instantiator =
                new Jackson2HalModule.HalHandlerInstantiator(
                    provider,
                    CurieProvider.NONE,
                    MessageResolver.DEFAULTS_ONLY);
    
            ObjectMapper mapper = converter.getObjectMapper();
            mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
            mapper.registerModule(module);
            mapper.setHandlerInstantiator(instantiator);
    
            try {
                String payloadJson = mapper.writeValueAsString(payload);
                byte[] bytes = payloadJson.getBytes();
                return bytes;
            } catch(JsonProcessingException exception) {
                return null;
            }
        }
    }
    

    I need to create a custom class since the method addSupportedMimeTypes is protected. The convertToInternalMethod holds the conversion logic. The Jackson2HalModule class is what I've been looking for all this time. This class is the reason why links becomes _links when a Response is sent with a Content-Type of application/hal+json.

    After creating this custom MessageConverter, I added this to the default converters of the application:

    @Configuration @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
        @Override
        public boolean configureMessageConverters(List<MessageConverter> messageConverters) {
            messageConverters.add(new HalMessageConverter());
            return false;
        }
    }