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

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.

Related

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

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.

Is it possible to use Qute templates in a custom extension?

I want to develop a Quarkus application that made of multiple independent components (custom extensions). Now, I need to each extension has own qute template; How can I do that?!
Here is a solution:
Put resources/templates/hello.html and this class in the deployment part of your custom extension.
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletionStage;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import io.quarkus.qute.Engine;
import io.quarkus.qute.Template;
import org.apache.commons.io.IOUtils;
#Path("hi")
public class TestQuteTemplateInDeployment {
public Template getTemplateFromFile(String path2template) {
Engine engine = Engine.builder().addDefaults().build();
ClassLoader classLoader = getClass().getClassLoader();
String content = "<!doctype html><html></html>";
try {
InputStream inputStream = classLoader.getResourceAsStream(path2template);
content = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
} catch (Exception e) {
;
}
Template helloTemplate = engine.parse(content);
return helloTemplate;
}
#GET
#Produces(MediaType.TEXT_HTML)
public CompletionStage<String> get() {
return getTemplateFromFile("templates/hello.html").data("msg", "Hi! I'm Here.").renderAsync();
}
}

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.

Spring Boot & Hibernate Validation's ConstraintMappingContributor

The hibernate validations documentation describes how to create ConstraintMappingContributors here.
It states:
You then need to specify the fully-qualified class name of the
contributor implementation in META-INF/validation.xml, using the
property key hibernate.validator.constraint_mapping_contributors. You
can specify several contributors by separating them with a comma.
Given I have many of these, what would be the most appropriate way to auto-discover these i.e. via #Component and add them dynamically at runtime to the ConstrainMappingConfiguration during Spring Boot startup.
For example.. if a developer creates a new ConstraintMappingContributor, it should be picked up and added automatically when spring boot starts, requiring no other file changes.
This is what I came up with, seems to be working for me.
package...
import org.hibernate.validator.spi.cfg.ConstraintMappingContributor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
#Configuration
public class ValidationConfiguration {
private final List<ConstraintMappingContributor> contributors;
public ValidationConfiguration(Optional<List<ConstraintMappingContributor>> contributors) {
this.contributors = contributors.orElseGet(ArrayList::new);
}
#Bean
public LocalValidatorFactoryBean validatorFactory() {
return new ValidatorFactoryBean(this.contributors);
}
}
package...
import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.internal.cfg.context.DefaultConstraintMapping;
import org.hibernate.validator.spi.cfg.ConstraintMappingContributor;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import javax.validation.Configuration;
import java.util.List;
public class ValidatorFactoryBean extends LocalValidatorFactoryBean {
private final List<ConstraintMappingContributor> contributors;
ValidatorFactoryBean(List<ConstraintMappingContributor> contributors) {
this.contributors = contributors;
}
#Override
protected void postProcessConfiguration(Configuration<?> cfg) {
if (cfg instanceof HibernateValidatorConfiguration) {
HibernateValidatorConfiguration configuration = (HibernateValidatorConfiguration) cfg;
this.contributors.forEach(contributor -> contributor.createConstraintMappings(() -> {
DefaultConstraintMapping mapping = new DefaultConstraintMapping();
configuration.addMapping(mapping);
return mapping;
}));
}
}
}
I invoke it like this...
if(SpringValidatorAdapter.class.isInstance(this.validatorFactory)){
SpringValidatorAdapter.class.cast(this.validatorFactory).validate(entity, errors);
}

Resources