Springboot: How to make Rest Service Controller non-blocking - java

I have a spring boot application which validates a file for a given client id and returns a json response of validation errors and warnings, while performing load test we noticed most the requests being blocked, so am trying to make our application non blocking by leveraging Spring's non blocking api
Below is my spring version
springBootVersion = '1.5.3.RELEASE'
springVersion = '4.3.8.RELEASE'
Below is my springboot ValidationController.groovy which is blocking requests
#Controller
#ResponseBody
class ValidationController {
#RequestMapping(value = "/validate", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
ResponseEntity<ValidationResult> validate(#RequestParam(value = "file", required = true) MultipartFile file,
#RequestParam(value = "client_id", required = true) String clientId)
{
if (clientId.isEmpty()) {
String msg = "client id is required"
if (LOGGER.isErrorEnabled()) {
LOGGER.error(msg)
}
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(msg);
}
String contentType = file.contentType.toLowerCase();
if (LOGGER.isDebugEnabled()) LOGGER.debug("content type = $contentType");
Client client = clientRepository.findByExternalId(clientId)
if (client == null) {
String msg = "client id is invalid"
if (LOGGER.isErrorEnabled()) {
LOGGER.error(msg)
}
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(msg);
}
if (file.isEmpty()) {
String msg = "file is empty"
if(LOGGER.isErrorEnabled()) {
LOGGER.error(msg)
}
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(msg);
}
ValidationResult result = validationService.validate(file, client);
return ResponseEntity.ok(result)
}
}
class ValidationResult {
private List<Warning> warnings
private List<Error> errors
//getters setters for warnings and errors
}
class Warning {
private String message
private String type
//getters setters for message and type
}
class Error {
private String message
private String type
//getters setters for message and type
}
I have modified my ValidationController.groovy as below
#Controller
#ResponseBody
class ValidationController {
#Autowired
#Qualifier("postRequestExecutorService")
private ExecutorService postRequestExecutor;
#RequestMapping(value = "/validate", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public DeferredResult<ResponseEntity<ValidationResult>> validate(#RequestParam(value = "file", required = true) MultipartFile file,
#RequestParam(value = "client_id", required = true) String clientId)
{
DeferredResult<ResponseEntity<ValidationResult>> deferredResult = new DeferredResult<>();
CompletableFuture.supplyAsync(() -> validate(clientId, file), postRequestExecutor)
.whenComplete((result, throwable) ->
{
deferredResult.setResult(result);
} );
}
private ResponseEntity<ValidationResult> validateLedes(String clientId, MultipartFile file) {
ValidationResult result;
try{
if (clientId.isEmpty()) {
String msg = messageSource.getMessage("client.id.required", null, Locale.getDefault())
LOGGER.error(msg)
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(msg);
}
String contentType = file.contentType.toLowerCase();
if (LOGGER.isDebugEnabled()) LOGGER.debug("content type = $contentType");
Client client = clientRepository.findByExternalId(clientId)
if (client == null) {
String msg = messageSource.getMessage("client.id.invalid", null, Locale.getDefault())
LOGGER.error(msg)
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(msg);
}
if (file.isEmpty()) {
String msg = messageSource.getMessage("ledes.file.empty", null, Locale.getDefault())
LOGGER.error(msg)
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(msg);
}
result = validationService.validate(file, Ledesxmlebilling21.class, client);
}
catch (Exception ex){
LOGGER.error("Exception in validateLedes = "+ex.message)
LOGGER.error("StackTrace in validateLedes = "+ex.stackTrace)
}
return ResponseEntity.ok(result)
}
}
And below is my ExecutorServiceConfiguration
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
#Configuration
public class RequestsExecuterServiceConfiguration {
/**
* Dedicated Thread Modeling to handle POST Requests
*/
#Bean
public ExecutorService postRequestExecutorService() {
final ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat("postRequestExecutor-%d")
.setDaemon(true)
.build();
ExecutorService es = Executors.newFixedThreadPool(10,threadFactory);
return es;
}
}
Since my controller is a groovy class am seeing some compiler errors for the CompletableFuture lambda expression, can someone please help me make it work for groovy controller?
UPDATE1
As per the Answer I've changed the labda expression to closure as below
#RequestMapping(value = "/validate", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public DeferredResult<ResponseEntity<ValidationResult>> validate(#RequestParam(value = "file", required = true) MultipartFile file,
#RequestParam(value = "client_id", required = true) String clientId)
{
DeferredResult<ResponseEntity<ValidationResult>> deferredResult = new DeferredResult<ResponseEntity<ValidationResult>>();
CompletableFuture.supplyAsync({ -> validateLedes(clientId, file) }, postRequestExecutor)
.whenComplete({ futureResult, throwable -> deferredResult.setResult(futureResult);})
deferredResult
}
With the above controller, am not getting below errors
2018-04-11 15:07:45 - Exception in validateLedes = failed to lazily initialize a collection of role: com.validation.entity.Client.ruleConfigurations, could not initialize proxy - no Session
2018-04-11 15:07:45 - StackTrace in validateLedes = org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:587), org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:204), org.hibernate.collection.internal.AbstractPersistentCollection.readSize(AbstractPersistentCollection.java:148), org.hibernate.collection.internal.PersistentSet.size(PersistentSet.java:143)
Looks like the issue is, Hibernate session is not bound to ExecutorService and the new thread in which validateLedes method is executing it's unable to read from the database, can someone please me to binding Hibernate session to the the ExecutorService's thread pool?

You can't just stick lambdas into Groovy (until Groovy 3)
You'll need to translate them to Closures, so for example:
() -> validate(clientId, file)
becomes:
{ -> validate(clientId, file) }
And
(result, throwable) ->
{
deferredResult.setResult(result);
}
would be:
{ result, throwable -> deferredResult.setResult(result) }

Related

How do I write a unit test for a controller class that has a Post method?

public class ReportEmailController {
#ApiImplicitParam(name = "Authorization", value = "JWT Token", required = true, dataType = "string", paramType = "header", example = Constants.SAMPLE_JWT_TOKEN)
#ApiOperation(value = "Details reporter email-service ", notes = "This method shows you thats sending summary id email info.")
#PostMapping(value = "/id/email-info", produces = "application/json")
public ResponseEntity<EmailInfoIdResponse> idEmailInfo(
#Valid #RequestHeader(value = "companyId", required = true) #ApiIgnore String companyId,
#Valid #RequestHeader(value = "user", required = true) #ApiIgnore String user,
#RequestBody IdEmailInfoRequest request) {
request.setCompanyId(companyId);
request.setAuthUser(user);
log.info("New ID Email Info Report request:{}", request);
List<EmailInfoIdParams> responseInfo = this.reporterService.getIdEmailInfo(request);
EmailInfoIdResponse response = new EmailInfoIdResponse();
response.setEmailInfoIdParams(responseInfo);
response.setStatus("SUCCESS");
response.setStatusCode(202);
response.setStatusDescription("SUCCESS");
return ResponseEntity.accepted().body(response);
}
}
#RunWith(MockitoJUnitRunner.class)
public class ReportEmailControllerTest {
#InjectMocks
private ReporterService reporterService;
#Mock
private EmailInfoRepository emailInfoRepository;
#InjectMocks
private ReportEmailController reportEmailController = new ReportEmailController();
#Test
public void testIdEmailInfo_whenRequestIsValidReturnValidList() {
getIdEmailInfoRequest();
List<EmailInfo> emailInfoList = new ArrayList<>();
EmailInfo emailInfo = new EmailInfo();
emailInfo.setMessage("test");
emailInfoList.add(emailInfo);
when(emailInfoRepository.findIdEmailInfos(idEmailInfoRequest)).thenReturn(emailInfoList);
log.info("emailInfoList:", emailInfoList);
ResponseEntity<EmailInfoIdResponse> response = reportEmailController.idEmailInfo("1","user,",idEmailInfoRequest);
assertNotEquals(response.getBody().getEmailInfoIdParams().size(),0);
}
}
My controller class and test class are as above. When I run this test, I get a NullPointerException error on the following lines in the test and controller class, but I do not expect this error to return. Could it be a problem with the definitions, can you please help, I couldn't solve it in any way.
-> Test class : ResponseEntity response = reportEmailController.idEmailInfo("1","user,",idEmailInfoRequest);
-> Controller class : List responseInfo = this.reporterService.getIdEmailInfo(request);
While unit testing controller, I would suggest you use Webclient. Also I see you are trying to test both the service as well as the controller in one go, which should not be done, you should unit test only one thing at a time.
As far as NPE go,
#InjectMocks
private ReporterService reporterService;
I believe it should be,
#Mock
private ReporterService reporterService;

Controller in DGS Netflix Graphql

We are developing a project Using Spring boot with DGS Netflix graphql. We are created all the schemas and datafethers which is working absolutely fine with a default endpoint "/graphql". we would like to expose this app with custom endpoing so we are trying to add a controller with a custom endpoint as below. But When i run the application and send a query, my data fetcher is called twice . first time called from controller and second time i believe from framework it self. Anybody has any thoughts on this why its being called twice and how to avoid it? You help is highly appreciated. Please see the below Datafetcher and Controller.
Controller:
#RestController
#RequestMapping("/sample-information/model")
#Slf4j
public class CustomController {
#Autowired
DgsQueryExecutor dgsQueryExecutor;
#PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, "application/graphql"})
public Mono<ResponseEntity<Object>> getDetails(#RequestBody String query,
#RequestHeader HttpHeaders headers
) {
GraphQLQueryInput inputs = null;
try {
inputs = ObjectMapperHelper.objectMapper.readValue(query, GraphQLQueryInput.class);
} catch (Exception e) {
log.error("TraceId: {} - Application Error: Error message: Error converting query to GraphQLQueryInput: {} "+ query);
}
if(inputs.getVariables() == null) {
inputs.setVariables(new HashMap<>());
}
if(inputs.getOperationName() == null) {
inputs.setOperationName("");
}
final String que = inputs.getQuery();
final Map<String, Object> var = inputs.getVariables();
final String opn = inputs.getOperationName();
ExecutionInput.Builder executionInput = ExecutionInput.newExecutionInput()
.query(inputs.getQuery())
.operationName(inputs.getOperationName())
.variables(inputs.getVariables());
return Mono.fromCallable(()-> {
return dgsQueryExecutor.execute(que, var, opn);
}).subscribeOn(Schedulers.elastic()).map(result -> {
return new ResponseEntity<>(result, HttpStatus.OK);
});
}
}
Datafetcher:
#DgsComponent
#Slf4j
public class SampleDataFetcher {
#Autowired
SampleBuilder sampleBuilder;
#DgsData(parentType = DgsConstants.QUERY_TYPE, field = DgsConstants.QUERY.SampleField)
public CompletableFuture<StoreDirectoryByStateResponse> getStoreDirectoryByState(#InputArgument String state, DataFetchingEnvironment dfe) throws BadRequestException {
Mono<StoreDirectoryByStateResponse> storeDirectoryResponse = null;
try {
sampleResponse = sampleBuilder.buildResponse(modelGraphQLContext);
} catch (BaseException e) {
}
return sampleResponse.map(response -> {
return response;
}).toFuture();
}
}

How to test getting parameters on the Rest service using the Post method

I'm trying to test getting parameters for processing a request using the Post method
#RestController
#RequestMapping("api")
public class InnerRestController {
…
#PostMapping("createList")
public ItemListId createList(#RequestParam String strListId,
#RequestParam String strDate) {
…
return null;
}
}
test method
variant 1
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class InnerRestControllerTest {
#LocalServerPort
private int port;
#Autowired
private TestRestTemplate restTemplate;
#Test
void innerCreatePublishList() {
String url = "http://localhost:" + this.port;
String uri = "/api/createList";
String listStr = "kl";
String strDate = "10:21";
URI uriToEndpoint = UriComponentsBuilder
.fromHttpUrl(url)
.path(uri)
.queryParam("strListId", listStr)
.queryParam("strDate ", strDate)
.build()
.encode()
.toUri();
ResponseEntity< ItemListId > listIdResponseEntity =
restTemplate.postForEntity(uri, uriToEndpoint, ItemListId.class);
}
}
variant 2
#Test
void createList() {
String uri = "/api/createList";
String listStr = "kl";
String strDate = "10:21";
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(uri)
.queryParam("strListId", listStr)
.queryParam("strDate ", strDate);
Map<String, String> map = new HashMap<>();
map.put("strListId", listStr);//request parameters
map.put("strDate", strDate);
ResponseEntity< ItemListId > listIdResponseEntity =
restTemplate.postForEntity(uri, map, ItemListId.class);
}
Update_1
In my project exceptions is handled thus:
dto
public final class ErrorResponseDto {
private String errorMsg;
private int status;
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd hh:mm:ss")
LocalDateTime timestamp;
...
handler
#RestControllerAdvice
public class ExceptionAdviceHandler {
#ExceptionHandler(value = PublishListException.class)
public ResponseEntity<ErrorResponseDto> handleGenericPublishListDublicateException(PublishListException e) {
ErrorResponseDto error = new ErrorResponseDto(e.getMessage());
error.setTimestamp(LocalDateTime.now());
error.setStatus((HttpStatus.CONFLICT.value()));
return new ResponseEntity<>(error, HttpStatus.CONFLICT);
}
}
In methods, where necessary, I throw a specific exception...
.w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.web.bind.MissingServletRequestParameterException:
Required String parameter 'strListId' is not present]
Who knows what the error is. Please explain what you need to add here and why ?
Let's take a look on declarations of postEntity:
postForEntity(URI url, Object request, Class<T> responseType)
...
postForEntity(String url, Object request, Class<T> responseType, Object... uriVariables)
As you can see, first argument is either URI or String with uriVariables, but second argument is always request entity.
In you first variant you put uri String as URI and then pass uriToEndpoint as request entity, pretending that it is request object. Correct solution will be:
ResponseEntity<ItemListId> listIdResponseEntity =
restTemplate.postForEntity(uriToEndpoint, null, ItemListId.class);
Addressing your comments.
If server responded with HTTP 409, RestTemplate will throw exception with content of your ErrorResponseDto. You can catch RestClientResponseException and deserialize server response stored in exception. Something like this:
try {
ResponseEntity<ItemListId> listIdResponseEntity =
restTemplate.postForEntity(uriToEndpoint, null,
ItemListId.class);
...
} catch(RestClientResponseException e) {
byte[] errorResponseDtoByteArray = e.getResponseBodyAsByteArray();
// Deserialize byte[] array using Jackson
}

How to mock a constructor object properties using Mockito?

I am trying to mock a constructor 'EmailParams' in my test class.
Mocking is failing since the constructor EmailParams mocks as null.
Below is my test method
#Test
public void getContactEmailsByFilterSuccessTest() throws Exception {
String contactId = "752";
String emailAddress = "test#gmail.com";
String emailType = "EW";
MockHttpServletRequest request = new MockHttpServletRequest();
when(helper.isNumeric(any(String.class))).thenReturn(true);
List<ContactXref> sourcedContacts = getContactXrefs();
when(contactXrefServiceMock.getContactsForId(contactId)).thenReturn(sourcedContacts);
EmailParams emailParams = new EmailParams("test#gmail.com", "EW", sourcedContacts.get(0).getContact().getContactId().toString());
List<Email> emailsList = getEmailsList();
when(emailServiceMock.getEmailByFilter(emailParams)).thenReturn(emailsList);
ResponseEntity<List<Email>> response = contactControllerMock.getContactEmailsByFilter(request, contactId, emailAddress, emailType);
Assert.assertEquals("getContactEmailsByFilterSuccessTest: Expected response code to be 200", "200",
response.getStatusCode().toString());
}
This is the method I am trying to mock. Test fails when its trying to mock the constructor
#GetMapping(value = "/{contactId}/" + UrlMapping.EMAILS, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<Email>> getContactEmailsByFilter(HttpServletRequest request,
#PathVariable(name = RequestParams.CONTACTID) String contacId,
#RequestParam(required = false, name = RequestParams.EMAILADDRESS) String emailAddress,
#RequestParam(required = false, name = RequestParams.EMAILTYPE) String emailType)
throws Exception {
ResponseEntity response = new ResponseEntity("Only numeric contactId is allowed", HttpStatus.BAD_REQUEST);
List<Email> emailList;
List<ContactXref> sourcedContacts;
if (helper.isNumeric(contactId)) {
sourcedContacts = contXrefService.getContactsForId(contactId);
EmailParams params = new EmailParams(emailAddress, emailType, sourcedContacts.get(0).getContact().getContactId().toString());
emailList = emailService.getEmailByFilter(params);
if (emailList != null) {
response = emailList.size() == 0 ? new ResponseEntity("No emails were found for the request", HttpStatus.NOT_FOUND) : new ResponseEntity(emailList, new HttpHeaders(), HttpStatus.OK);
} else {
response = new ResponseEntity("Encountered exception in retrieving emails", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
return response;
}
Here is my class which has the constructor.
public class EmailParams {
String email;
String emailType;
String ptyId;
public EmailParams() {
super();
}
public EmailParams(String pEmail, String pEmailType, String pPtyId) {
email = pEmail;
emailType = pEmailType;
ptyId = pPtyId;
}
}
How to mock it properly? thanks in advance.
If the equals method is not overridden in EmailParams class by default Mockito uses Object.equals to compare the EmailParams passed to getEmailByFilter method. In your case both object properties have same values but still they are different objects. So either override the equals method in EmailParams or
use ArgumentMatchers.argThat
when(emailServiceMock.getEmailByFilter(ArgumentMatchers.argThat(p -> p.getPEmail().equals("test#gmail.com") && condition2 && condition3 )))
.thenReturn(emailsList);
So emailService is expected to be invoked with emailParams. The emailParams is constructed using emailAddress, emailType and a contactId. If you look closely, you'll realize that sourcedContacts in your controller is the result of contXrefService.getContactsForId(contactId).
Why is this a problem? Well, look at this line in your test:
when(contactXrefServiceMock.getContactsForEcmId(contactId)).thenReturn(sourcedContacts)
You're mocking getContactsForEcmId to return the sourcedContacts. Instead, you should be mocking getContactsForId.

How can I subscribe to incoming SSE events using Spring

I wrote a Spring RestController that returns a SseEmitter (for server-sent-event), and adds HATEOAS links to each event. Here is a simplified but working example of this controller:
package hello;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import hello.Greeting.Status;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
#RestController
public class GreetingController {
private static final Logger log = LoggerFactory.getLogger(GreetingController.class);
private static final String template = "Hello, %s!";
class GreetingRequestHandler implements Runnable {
private ResponseBodyEmitter emitter;
private Greeting greeting;
public GreetingRequestHandler(final ResponseBodyEmitter emitter, final Greeting greeting) {
this.emitter = emitter;
this.greeting = greeting;
}
#Override
public void run() {
try {
log.info(this.greeting.toString());
this.emitter.send(this.greeting);
Thread.sleep(5000);
if (Status.COMPLETE.equals(this.greeting.getStatus())) {
this.emitter.complete();
} else {
this.greeting.incrementStatus();
new Thread(new GreetingRequestHandler(this.emitter, this.greeting)).start();
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
#RequestMapping(path = "/greeting")
public SseEmitter greeting(#RequestParam(value = "name", defaultValue = "World") final String name) {
SseEmitter emitter = new SseEmitter();
Greeting greeting = new Greeting(String.format(template, name));
greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel());
new Thread(new GreetingRequestHandler(emitter, greeting)).start();
log.info("returning emitter");
return emitter;
}
}
The Greeting class is the following:
package hello;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
#JsonIgnoreProperties(ignoreUnknown = true)
public class Greeting extends ResourceSupport {
private final String content;
private final static AtomicInteger idProvider = new AtomicInteger();
private int greetingId;
private Status status;
enum Status {
ENQUEUED,
PROCESSING,
COMPLETE;
}
#JsonCreator
public Greeting(#JsonProperty("content") final String content) {
this.greetingId = idProvider.addAndGet(1);
this.status = Status.ENQUEUED;
this.content = content;
}
public Status getStatus() {
return this.status;
}
protected void setStatus(final Status status) {
this.status = status;
}
public int getGreetingId() {
return this.greetingId;
}
public String getContent() {
return this.content;
}
#Override
public String toString() {
return "Greeting{id='" + this.greetingId + "', status='" + this.status + "' content='" + this.content + "', " + super.toString() + "}";
}
public void incrementStatus() {
switch (this.status) {
case ENQUEUED:
this.status = Status.PROCESSING;
break;
case PROCESSING:
this.status = Status.COMPLETE;
break;
default:
break;
}
}
}
This code works perfectly. If I try to reach the REST service using a web browser, I see events appearing with correct content and link.
The result looks like (each event appearing 5 seconds after the previous one):
data:{"content":"Hello, Kraal!","greetingId":8,"status":"ENQUEUED","_links":{"self":{"href":"http://localhost:8080/greeting?name=Kraal"}}}
data:{"content":"Hello, Kraal!","greetingId":8,"status":"PROCESSING","_links":{"self":{"href":"http://localhost:8080/greeting?name=Kraal"}}}
data:{"content":"Hello, Kraal!","greetingId":8,"status":"COMPLETE","_links":{"self":{"href":"http://localhost:8080/greeting?name=Kraal"}}}
Now I need to call this REST service and read these events from another Spring application... But I have no clue how to write the client code using Spring. This does not work as RestTemplate is designed for synchronous client side HTTP access...
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new Jackson2HalModule());
// required for HATEOAS
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/hal+json"));
converter.setObjectMapper(mapper);
// required in order to be able to read serialized objects
MappingJackson2HttpMessageConverter converter2 = new MappingJackson2HttpMessageConverter();
converter2.setSupportedMediaTypes(MediaType.parseMediaTypes("application/octet-stream"));
converter2.setObjectMapper(mapper);
// required to understand SSE events
MappingJackson2HttpMessageConverter converter3 = new MappingJackson2HttpMessageConverter();
converter3.setSupportedMediaTypes(MediaType.parseMediaTypes("text/event-stream"));
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(converter);
converters.add(converter2);
converters.add(converter3);
// probably wrong template
RestTemplate restTemplate = new RestTemplate();
restTemplate = new RestTemplate(converters);
// this does not work as I receive events and no a single object
Greeting greeting = restTemplate.getForObject("http://localhost:8080/greeting/?name=Kraal", Greeting.class);
log.info(greeting.toString());
The error message I get is:
Caused by: com.fasterxml.jackson.core.JsonParseException: Unrecognized token 'data': was expecting ('true', 'false' or 'null')
Indeed each event is a SSE event and starts with "data:"...
So the questions are:
what ObjectMapper module should I register in order to be able to map SSE with Jackson ?
how can I subscribe to incoming SSE events (observer pattern) using Spring ?
Thanks in advance.
Side note: As I'm struggling doing it using Spring I tried to do it using Jersey SSE support as follows. Using Jersey I receive events as expected, but then I can't cast them to a Greeting class (for the same reason as above I guess which is that I don't have the right converter module .):
Client client = ClientBuilder.newBuilder().register(converter).register(SseFeature.class).build();
WebTarget target = client.target("http://localhost:8080/greeting/?name=Kraal");
EventInput eventInput = target.request().get(EventInput.class);
while (!eventInput.isClosed()) {
final InboundEvent inboundEvent = eventInput.read();
if (inboundEvent == null) {
// connection has been closed
break;
}
// this works fine and prints out events as they are incoming
System.out.println(inboundEvent.readData(String.class));
// but this doesn't as no proper way to deserialize the
// class with HATEOAS links can be found
// Greeting greeting = inboundEvent.readData(Greeting.class);
// System.out.println(greeting.toString());
}
as per the documentation
you can use inboundEvent.readData(Class<T> type)

Categories