I am trying to get Header info from Request Controller and read into IntegrationFlow - spring

I wanted to understand where is best location to read headers and use them inside my IntegrationFlow layer.
ServiceController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
#RequestMapping("/api/v1/integration")
public class ServiceController {
#Autowired
private ServiceGateway gateway;
#GetMapping(value = "info")
public String info() {
return gateway.info();
}
}
ServiceGateway.java
import org.springframework.integration.annotation.Gateway;
import org.springframework.integration.annotation.MessagingGateway;
#MessagingGateway
public interface ServiceGateway {
#Gateway(requestChannel = "integration.info.gateway.channel")
public String info();
}
ServiceConfig.java
import java.net.URISyntaxException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.integration.annotation.IntegrationComponentScan;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.http.dsl.Http;
import org.springframework.messaging.MessageHeaders;
#Configuration
#EnableIntegration
#IntegrationComponentScan
public class ServiceConfig {
#Bean
public IntegrationFlow info() throws URISyntaxException {
String uri = "http://localhost:8081/hellos/simpler";
return IntegrationFlows.from("integration.info.gateway.channel")
.handle(Http.outboundGateway(uri).httpMethod(HttpMethod.POST).expectedResponseType(String.class)).get();
}
}
From Consumer I am receiving some Header meta data. I want to know in above flow whether it is good idea from following approaches:
Read headers in Controller and then pass through into my IntegrationFlow: For this I am not aware how to pass through.
Is there best or any way exist to read request headers into IntegrationFlow layer?
For this second approach I have tried below code but runtime I am getting error as channel is one way and hence stopping the flow.
return IntegrationFlows.from("integration.info.gateway.channel").handle((request) -> {
MessageHeaders headers = request.getHeaders();
System.out.println("-----------" + headers);
}).handle(Http.outboundGateway(uri).httpMethod(HttpMethod.POST).expectedResponseType(String.class)).get();
My problem is how to send request parameters from incoming call to carry those internally invoking another rest call. Here I wanted to transform the data from request headers and construct into new json body and then send this to http://localhost:8081/hellos/simpler URL.
The flow:
I am trying to construct this RequestBody before sending to internal REST POST call:

A gateway method with no paylaod is for receiving data, not requesting it.
https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#gateway-calling-no-argument-methods
Add a #Header annotated parameter to the gateway.
https://docs.spring.io/spring-integration/docs/current/reference/html/messaging-endpoints.html#gateway-configuration-annotations
#MessagingGateway
public interface ServiceGateway {
#Gateway(requestChannel = "integration.info.gateway.channel")
public String info("", #Header("x-api") String xApi);
}
This will send a message with an empty string as the payload with the header set.

Related

How to post data as csv file to rest entpoint in spring boot using WebClient

I'm trying to migrate data from an in house database to a software. The software has a REST api for this purpose, that expects a csv file.
A working curl call for this API endpoint looks like this:
curl -isk POST -H "customHeaderName:customHeaderValue" -H "Authorization: bearer $TOKEN" -F "data=#accounts.csv" <apiBaseUrl>/gate/account/import/group-accounts
My plan is to post the data directly to the REST endpoint with a spring boot application, without crating a physical csv file first.
My implementation looks like this, with "csvString" beeing a csv formatted String (e.g.: "acc_id,acc_name,acc_desc\r\n1,john.doe,this is john\r\n2,peter.parker,this is peter"):
(I removed this code and added the current version below.)
When I call postAccountsAndGroups(csvString); I get a 415 response indicating that my request Body is not a propper csv file.
EDIT:
It seems like the API endpoint requires a Multipart Form. Therfore I came up with something like this:
import static org.springframework.web.util.UriComponentsBuilder.fromUriString;
import my.package.common.configuration.WebClientConfig;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.service.spi.ServiceException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
#Service
#Slf4j
public class MyApiImpl implements MyApi {
private final WebClient client;
private final String apiBaseUrl;
public MyApiImpl(
#Qualifier(WebClientConfig.MY_API_CLIENT_CONFIG) WebClient client,
#Value("${external.api.myapi.baseUrl}") String apiBaseUrl) {
this.client = client;
this.apiBaseUrl = apiBaseUrl;
}
#Override
public Mono<HttpStatus> postAccountsAndGroups(String csvString) {
MultipartBodyBuilder builder = new MultipartBodyBuilder();
Resource byteArrayResource = new ByteArrayResource(csvString.getBytes(StandardCharsets.UTF_8));
builder.part("data", byteArrayResource);
return client.post()
.uri(createAccountsUri())
.header("customHeaderName", "customHeaderValue")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(HttpStatus.class).thenReturn(response.statusCode());
} else {
throw new ServiceException("Error uploading file");
}
});
}
private URI createAccountsUri() {
return fromUriString(apiBaseUrl).path("/gate/account/import/group-accounts").build().toUri();
}
}
Now I get 400 Bad Request as response though.
I stil havend found a way to implement my prefered solution. However I came up with this workaround, that relies on persisting the csv file:
In my case I chose "/tmp/account.csv" as file path since my application runs in a docker container with linux os. On a Windows machine you could use something like "C:/myapp/account.csv". The file path is injected vie the application.properties file using the custom value "migration.files.accounts" so it can be configured later.
import static org.springframework.web.util.UriComponentsBuilder.fromUriString;
import my.package.common.configuration.WebClientConfig;
import java.io.File;
import java.io.PrintWriter;
import java.net.URI;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.service.spi.ServiceException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.MultipartBodyBuilder;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
#Service
#Slf4j
public class PrimedexApiImpl implements PrimedexApi {
private final WebClient client;
private final String apiBaseUrl;
private final FileSystemResource accountsFile;
private final String accountsFilePath;
public PrimedexApiImpl(
#Qualifier(WebClientConfig.MY_API_CLIENT_CONFIG) WebClient client,
#Value("${external.api.api.baseUrl}") String apiBaseUrl,
#Value("${migration.files.accounts}") String accountsFilePath) {
this.client = client;
this.apiBaseUrl = apiBaseUrl;
this.accountsFilePath = accountsFilePath;
this.accountsFile = new FileSystemResource(accountsFilePath);
}
#Override
public Mono<HttpStatus> postAccountsAndGroups(String csvString) {
File csvOutputFile = new File(accountsFilePath);
if (csvOutputFile.delete()) {
log.info("An old version of '{}' was deleted.", accountsFilePath);
}
try (PrintWriter pw = new PrintWriter(csvOutputFile)) {
pw.print(csvString);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("data", accountsFile);
return client.post()
.uri(createAccountsUri())
.header("customHeaderName", "customHeaderValue")
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(BodyInserters.fromMultipartData(builder.build()))
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.releaseBody().thenReturn(response.statusCode());
} else {
throw new ServiceException("Error uploading file");
}
});
}
private URI createAccountsUri() {
return fromUriString(apiBaseUrl).path("/gate/account/import/group-accounts").build().toUri();
}
}
I used spring-boot-starter-parent version 2.6.3 for this project.

How to use Spring Security with load balancer?

I am new at loadBalancing so please I need help and thats what i did :
i built 2 services as 2 apps (A,B) I used spring security on both of them
(both of them are restfull api , they have theymleaf and full frontEnd pages ),
then i had made another app as spring cloud loadbalancer .
when i send a request , it go from loadbalancer app to one of the 2 services but the problem is when iam not authenticated the response will be empty , it wont take me to the default login page as usual as when i use the normal A app directly , and when i go to pages that does not need to be authenticated to get to it , it is returned without my css/js styles
this is my A app controller ( it is returning view not json )
package com.hariri_stocks.controllers;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.hariri_stocks.models.Estates;
import com.hariri_stocks.models.SoldEstates;
import com.hariri_stocks.models.Users;
import com.hariri_stocks.services.estatesService;
#Controller
public class LoginController {
#Autowired
estatesService ES;
#GetMapping(value = "/")
public String login() {
return "/signIn-up.html";
}
#GetMapping(value = "/dashboard")
public String dashboard(Model model ,#RequestParam(required = false) String add_result
,#RequestParam(required = false) String alert_err) {
List<Estates> estates = ES.findAll();
model.addAttribute("estates",estates);
return "/dashboard";
}
#GetMapping(value = "/dashboard/unSold")
public String unselled_stocks(Model model) {
List<Estates> estates = ES.findUnsold();
if(estates.size() > 0)
model.addAttribute("estates",estates);
else
model.addAttribute("error","there is no sold estates yet !!");
return "/dashboard";
}
#Value(value = "${server.port}")
String port_num;
#GetMapping("/port")
public String hello() {
return port_num;
}
}
and this is my loadbalancer controller iam using #restcontroller
package com.hariri_loadbalancer;
import reactor.core.publisher.Mono;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.reactive.function.client.WebClient;
#SpringBootApplication
#RestController
public class UserApplication {
private final WebClient.Builder loadBalancedWebClientBuilder;
private final ReactorLoadBalancerExchangeFilterFunction lbFunction;
public UserApplication(WebClient.Builder webClientBuilder,
ReactorLoadBalancerExchangeFilterFunction lbFunction) {
this.loadBalancedWebClientBuilder = webClientBuilder;
this.lbFunction = lbFunction;
}
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
#RequestMapping("/port")
public Mono<String> showMePort() {
return loadBalancedWebClientBuilder.build().get().uri("http://hariri/port")
.retrieve().bodyToMono(String.class);
}
#RequestMapping("/")
public Mono<String> showMainPage() {
return loadBalancedWebClientBuilder.build().get().uri("http://hariri/")
.retrieve().bodyToMono(String.class);
}
}
So what should I do? I feel that what I am doing is stupid,
should I move all my Thymleaf pages to the loadbalancer maybe , so that the a app return what it want to return with #restController then the loadbalancer use #controller to get to the styling front pages or there is a way , and for the security , should i implement the spring security with the loadbalancer instead of the A,B apps
.........................
8080 is loadBalancer port
9091 is A app port
so it seams that when A is returning the html page , the html is searching for the css at the loadbalancer machin at 8080 , while they are existing at A app on 9091
bodyToMono decodes the body but you are not handling headers.
On spring security there is very likely a redirection to the login page ... so it wont work if you only attend to the body. This might be also affecting styles somehow.
Check something like this:
How to extract response header & status code from Spring 5 WebClient ClientResponse

Get response body from NoFallbackAvailableException in spring cloud circuit breaker resilience4j

I want to call a third party API. I use spring cloud circuit breaker resilience4j.
Here is my service class :
package ir.co.isc.resilience4jservice.service;
import ir.co.isc.resilience4jservice.model.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
#Service
public class EmployeeService {
#Autowired
private RestTemplate restTemplate;
#Autowired
private CircuitBreakerFactory circuitBreakerFactory;
public Employee getEmployee() {
try {
String url = "http://localhost:8090/employee";
CircuitBreaker circuitBreaker = circuitBreakerFactory.create("circuit-breaker");
return circuitBreaker.run(() -> restTemplate.getForObject(url, Employee.class));
} catch (NoFallbackAvailableException e) {
//I should extract error response body and do right action then return correct answer
return null;
}
}
}
ResilienceConfig:
package ir.co.isc.resilience4jservice.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
#Configuration
public class CircuitBreakerConfiguration {
#Bean
public Customizer<Resilience4JCircuitBreakerFactory> defaultCustomizer() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10)
.minimumNumberOfCalls(10)
.failureRateThreshold(25)
.permittedNumberOfCallsInHalfOpenState(3)
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(4))
.build();
return factory ->
factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(circuitBreakerConfig)
.timeLimiterConfig(timeLimiterConfig)
.build());
}
}
in some situation third party api return ResponseEntity with statusCode = 500 and
body = {"errorCode":"CCBE"}.
response is look like this :
[503] during [POST] to [http://localhost:8090/employee]:[{"errorCode":"CCBE"}]
When I call this API and get internal server error with body, my catch block catchs api response.
In catch block I need retrieve response body and do some actions according to errorCode.
But I can not do this.
How can I extract body in this situation?

How to make Spring boot CSV message converter display CSV inline and not download when using a browser

I created a spring starter project in eclipse . Most of the code was from this link https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/csv-msg-converter.html.
I added content negotiation configuration to accept headers, path extension and parameters. It works great from postman.
But when I try in a browser http://localhost:8080/employeelist.csv. In all the cases CSV is getting downloaded in a file. I want it displayed inline on the browser. I tried to set content disposition as inline in Request mapping, http output message header but still CSV is always getting downloaded.
What should I be doing to get csv displayed inline? I had previously successfully displayed CSV inline in a browser by having separate request mapping method for CSV and make the method return void and accept httpservletresponse as parameter. But I want to use content negotiation and a single method for all formats - XML, CSV, json. Whatever format selected should be displayed inline in the browser.
Is that possible ?
Thanks a lot for your time.
Update : added portions of code which were edited
package ti.projects;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.util.List;
#SuppressWarnings("deprecation")
#EnableWebMvc
#Configuration
#ComponentScan("ti.projects")
public class AppConfig extends WebMvcConfigurerAdapter {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter<>());
}
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.favorPathExtension(true).favorParameter(true).parameterName("mediaType").ignoreAcceptHeader(false)
.useJaf(false).mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("csv", new MediaType("text", "csv"));
}
}
package ti.projects;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import java.util.Arrays;
import java.util.List;
#Controller
public class ExampleController {
#RequestMapping(
value = "/newEmployee",
consumes = "text/csv",
produces = MediaType.TEXT_PLAIN_VALUE,
method = RequestMethod.POST)
#ResponseBody
#ResponseStatus(HttpStatus.OK)
public String handleRequest (#RequestBody EmployeeList employeeList) {
System.out.printf("In handleRequest method, employeeList: %s%n", employeeList.getList());
String s = String.format("size: " + employeeList.getList().size());
System.out.println(s);
return s;
}
#RequestMapping(
value = "/employeeList",
produces = {"text/csv", "application/json"},
method = RequestMethod.GET
)
#ResponseBody
#ResponseStatus(HttpStatus.OK)
public EmployeeList handleRequest2 () {
List<Employee> list = Arrays.asList(
new Employee("1", "Tina", "111-111-1111"),
new Employee("2", "John", "222-222-2222")
);
EmployeeList employeeList = new EmployeeList();
employeeList.setList(list);
return employeeList;
}
}
package ti.projects;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class ContentNegotiationApplication {
public static void main(String[] args) {
SpringApplication.run(ContentNegotiationApplication.class, args);
}
}
The browser (should) use the provided mime type to decide how to display or process the response. What should work is using a MIME of text/plain to let the browser render the received content as text.
You can set the MIME type of your response in your spring Controller like this:
#GetMapping(produces = MediaType.TEXT_PLAIN_VALUE)
public String renderCsv() {...}
If you want to offer different MIME types with one method you have three options:
Use query parameter (e.g. ...?contentType=json)
Use path parameter (e.g..../{contentType})
Use accept header of client (preferably?)
You can register different MessageConverter for each contentType and configure a ContentNegotiationConfigurer to automatically choose the correct converter depending on given MIME type and your preferences.
I'll try to attach an example tonight.

Mapping RestTemplate response to java Object

I am using RestTemplate get data from remote rest service and my code is like this.
ResponseEntity<List<MyObject >> responseEntity = restTemplate.exchange(request, responseType);
But rest service will return just text message saying no record found if there are no results and my above line of code will throw exception.
I could map result first to string and later use Jackson 2 ObjectMapper to map to MyObject.
ResponseEntity<String> responseEntity = restTemplate.exchange(request, responseType);
String jsonInput= response.getBody();
List<MyObject> myObjects = objectMapper.readValue(jsonInput, new TypeReference<List<MyObject>>(){});
But I don't like this approach. Is there any better solution for this.
First of all you could write a wrapper for the whole API. Annotate it with #Component and you can use it wherever you want though Springs DI. Have a look at this example project which shows of generated code for a resttemplate client by using swagger codegen.
As you said you tried implementing a custom responserrorhandler without success I assume that the API returns the response body "no record found" while the status code is 200.
Therefore you could create a custom AbstractHttpMessageConverter as mentioned in my second answer. Because you are using springs resttemplate which is using the objectmapper with jackson we don't event have to use this very general super class to create our own. We can use and extend the more suited AbstractJackson2HttpMessageConverter class.
An implementation for your specific use case could look as follows:
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
public class WeirdAPIJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
public static final String NO_RECORD_FOUND = "no record found";
public WeirdAPIJackson2HttpMessageConverter() {
// Create another constructor if you want to pass an already existing ObjectMapper
// Currently this HttpMessageConverter is applied for every MediaType, this is application-dependent
super(new ObjectMapper(), MediaType.ALL);
}
#Override
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
try (BufferedReader br = new BufferedReader(new InputStreamReader(inputMessage.getBody(), DEFAULT_CHARSET))) {
String responseBodyStr = br.lines().collect(Collectors.joining(System.lineSeparator()));
if (NO_RECORD_FOUND.equals(responseBodyStr)) {
JavaType javaType = super.getJavaType(type, contextClass);
if(Collection.class.isAssignableFrom(javaType.getRawClass())){
return Collections.emptyList();
} else if( Map.class.isAssignableFrom(javaType.getRawClass())){
return Collections.emptyMap();
}
return null;
}
}
return super.read(type, contextClass, inputMessage);
}
}
The custom HttpMessageConverter is checking the response body for your specific "no record found". If this is the case, we try to return a default value depending on the generic return type. Atm returning an empty list if the return type is a sub type of Collection, an empty set for Set and null for all other Class types.
Furthermore I created a RestClientTest using a MockRestServiceServer to demonstrate you how you can use your RestTemplate within the aforementioned API wrapper component and how to set it up to use our custom AbstractJackson2HttpMessageConverter.
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.ExpectedCount;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;
import java.util.List;
import java.util.Optional;
import static org.junit.Assert.*;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus;
#RunWith(SpringRunner.class)
#ContextConfiguration(classes = {RestTemplateResponseErrorHandlerIntegrationTest.MyObject.class})
#RestClientTest
public class RestTemplateResponseErrorHandlerIntegrationTest {
static class MyObject {
// This just refers to your MyObject class which you mentioned in your answer
}
private final static String REQUEST_API_URL = "/api/myobjects/";
private final static String REQUEST_API_URL_SINGLE = "/api/myobjects/1";
#Autowired
private MockRestServiceServer server;
#Autowired
private RestTemplateBuilder builder;
#Test
public void test_custom_converter_on_weird_api_response_list() {
assertNotNull(this.builder);
assertNotNull(this.server);
RestTemplate restTemplate = this.builder
.messageConverters(new WeirdAPIJackson2HttpMessageConverter())
.build();
this.server.expect(ExpectedCount.once(), requestTo(REQUEST_API_URL))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK).body(WeirdAPIJackson2HttpMessageConverter.NO_RECORD_FOUND));
this.server.expect(ExpectedCount.once(), requestTo(REQUEST_API_URL_SINGLE))
.andExpect(method(HttpMethod.GET))
.andRespond(withStatus(HttpStatus.OK).body(WeirdAPIJackson2HttpMessageConverter.NO_RECORD_FOUND));
ResponseEntity<List<MyObject>> response = restTemplate.exchange(REQUEST_API_URL,
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<MyObject>>() {
});
assertNotNull(response.getBody());
assertTrue(response.getBody().isEmpty());
Optional<MyObject> myObject = Optional.ofNullable(restTemplate.getForObject(REQUEST_API_URL_SINGLE, MyObject.class));
assertFalse(myObject.isPresent());
this.server.verify();
}
}
What I usually do in my projects with restTemplate is save the response in a java.util.Map and create a method that converts that Map in the object I want. Maybe saving the response in an abstract object like Map helps you with that exception problem.
For example, I make the request like this:
List<Map> list = null;
List<MyObject> listObjects = new ArrayList<MyObject>();
HttpHeaders headers = new HttpHeaders();
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);
if (response != null && response.getStatusCode().value() == 200) {
list = (List<Map>) response.getBody().get("items"); // this depends on the response
for (Map item : list) { // we iterate for each one of the items of the list transforming it
MyObject myObject = transform(item);
listObjects.add(myObject);
}
}
The function transform() is a custom method made by me: MyObject transform(Map item); that receives a Map object and returns the object I want. You can check if there was no records found first instead of calling the method transform.

Resources