Invalid Syntax error while consuming graphql API - graphql

I am also facing some issue while hitting Graphql APIs through postman.
http://testEnvrironment/restcall/getReports
POST body
getReports(id:15){
isApproved
}
}
Entries of sample.graphqls file is
schema{
query: Query
}
type Query{
getReports(id: Long): PublishedReportInternal
}
type PublishedReportInternal{
sourceReport:ObjectRef
isApproved:Boolean
ignoreOutputFormatId:Boolean
}
type ObjectRef{
id : Long
qualifier : String
}
#Override
public ResponseEntity<Object> getReports(String query) {
ExecutionResult execute = graphQLService.getGraphQL().execute(query);
System.out.println("query::"+query);
System.out.println("execute result...."+execute);
return new ResponseEntity<>(execute, HttpStatus.OK);
}
#Service
public class CustomDataFetcher implements DataFetcher<PublishedReportInternal> {
#Inject
PublishRunReportInternalRestService service;
#Override
public PublishedReportInternal get(DataFetchingEnvironment environment) {
System.out.println("Entered into DataFetcher class...");
String reportId=environment.getArgument("id");
System.out.println("reportId is ::"+reportId);
if(!StringUtils.isEmpty(reportId)) {
return service.getReportById(new Long(reportId));
}
else {
return null;
}
}
}
Below is the implementation of GraphQLService
#Service
public class GraphQLService {
#Value("classpath:sample.graphqls")
private Resource schemaResource;
private GraphQL graphQL;
#Inject
private CustomDataFetcher customDataFetcher;
#PostConstruct
private void loadSchema() throws IOException {
// get the schema
File schemaFile = schemaResource.getFile();
System.out.println(schemaFile.getAbsolutePath());
// parse schema
TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaFile);
RuntimeWiring wiring = buildRuntimeWiring();
GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(typeRegistry, wiring);
graphQL = GraphQL.newGraphQL(schema).build();
}
private RuntimeWiring buildRuntimeWiring() {
return RuntimeWiring.newRuntimeWiring().type("Query", typeWiring -> typeWiring
.dataFetcher("getReports", customDataFetcher)).build();
}
public GraphQL getGraphQL() {
return graphQL;
}
}
Rest endpoint will be
#POST
#Path(value = "/getReports")
#Consumes
(value = MediaType.APPLICATION_JSON)
#Produces
(value = MediaType.APPLICATION_JSON) ResponseEntity<Object> getReports( String query);
Please try to help here as I am stuck completely with this problem.
Following jar has been used by me for this POC
compile files('libs/new/graphiql-spring-boot-starter-5.0.2.jar')
compile files('libs/new/graphql-java-9.2.jar')
compile files('libs/new/graphql-java-servlet-6.1.2.jar')
compile files('libs/new/graphql-java-tools-5.2.4.jar')
compile files('libs/new/graphql-spring-boot-autoconfigure-5.0.2.jar')
compile files('libs/new/graphql-spring-boot-starter-5.0.2.jar')
compile files('libs/new/java-dataloader-2.0.2.jar')
compile files('libs/new/antlr4-runtime-4.7.1.jar')
compile files('libs/new/reactive-streams-1.0.3.jar')
Above code snippet contains all the information which I have used to perform this POC, but while performing this POC I am consistently getting "Invalid Syntax" error. Please help in resolving this issue.
Regards
Kushagra

Related

FeignException com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.http.ResponseEntity`

Any Help please !!
I receive this error when I'm calling my endpoint which call Feign in the background :
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of
`org.springframework.http.ResponseEntity` (no Creators, like default constructor, exist): cannot deserialize
from Object value (no delegate- or property-based Creator)
at [Source: (BufferedReader); line: 1, column: 2]
This is my endpoint inside Controller :
#RestController
#RequestMapping(Routes.URI_PREFIX)
public class CartoController {
#Autowired
private ReadCartographyApiDelegate readCartographyApiDelegate;
#GetMapping(value = "/cartographies/{uid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseWrapper<ReadCartographyResponse> readCarto(HttpServletRequest request,
#PathVariable(name = "uid") String uid) {
ResponseEntity<ReadCartographyResponse> result ;
try {
result = readCartographyApiDelegate.readCartography(uid);
}catch (Exception e){
throw new TechnicalException("Error during read Carto");
}
return responseWrapperWithIdBuilder.of(result.getBody());
}
}
Interface ReadCartographyApiDelegate generated automatically by openApi from yaml file :
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "...")
public interface ReadCartographyApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
default ResponseEntity<ReadCartographyResponse> readCartography(String uid) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "null";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
This my ReadCartoApiDelegateImpl which implements ReadCartographyApiDelegate interface :
#Service
public class ReadCartographyApiDelegateImpl implements ReadCartographyApiDelegate {
private EcomGtmClient ecomGtmClient;
public ReadCartographyApiDelegateImpl(EcomGtmClient ecomGtmClient) {
this.ecomGtmClient = ecomGtmClient;
}
#Override
public ResponseEntity<ReadCartographyResponse> readCartography(String uid) {
ResponseEntity<ReadCartographyResponse> response = ecomGtmClient.readCartography(uid);
return response;
}
}
This is the feign client :
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ResponseEntity<ReadCartographyResponse> readCartography(#PathVariable("uid") String uid);
}
The problem is that ResponseEntity (spring class) class doesn't contain default constructor which is needed during creating of instance. is there Any config to resolve this issue ?
If you want access to the body or headers on feign responses, you should use the feign.Response class. ResponseEntity does not work with feign because it is not meant to. I think it is best if you just return Response from your feign client method. You should then be able to pass the body to the ResponseEntity instance in the Controller.
What is your reason to even use the response-wrapper, i can't really figure that out from your code?
Sadly I couldn't find any documentation on the Response class, but here's the link to the source on GitHub.
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Response.java
My Suggestion would be
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ReadCartographyResponse readCartography(#PathVariable("uid") String uid);
}
#RestController
#RequestMapping(Routes.URI_PREFIX)
public class CartoController {
#Autowired
private ReadCartographyApiDelegate readCartographyApiDelegate;
#GetMapping(value = "/cartographies/{uid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseWrapper<ReadCartographyResponse> readCarto(HttpServletRequest request,
#PathVariable(name = "uid") String uid) {
ReadCartographyResponse result ;
try {
result = readCartographyApiDelegate.readCartography(uid);
}catch (Exception e){
throw new TechnicalException("Error during read Carto");
}
// I don't know where you get the builder from, so I assume it does something import and is needed
return responseWrapperWithIdBuilder.of(result);
}
}
Of course you'd also have to change all intermediate classes.
The Response Output was the correct Object that I have to put, cause every time I need to check the status from my feign client endpoint to do différent logic
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ReadCartographyResponse readCartography(#PathVariable("uid") String uid);
}

Global Exception Handling in Spring Cloud Function on AWS Lambda Platform

I am using spring cloud function on AWS lambda. I am trying to achieve global exception handling like Spring Boot using #ExceptionHandler annotation. But this method is not getting executed and I am getting 500 for any type of exception.
Sample code is below-
#SpringBootApplication
public class App{
public static void main( String[] args ){
SpringApplication.run(App.class, args);
}
#Bean
public Function<Message<User>, User> getUser(){
return (message)->{
User u = message.getPayload();
if(u==null){
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"No user details provided");
}
return u;
}
}
#ExceptionHandler(ResponseStatusException.class)
public APIGatewayProxyResponseEvent handleException(ResponseStatusException e){
APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
response.setStatusCode(e.getRawStatusCode());
response.setBody(e.getMessage());
return response;
}
}
I am getting 500 in response instead Bad Request. Is there any way to achieve this scenario ?
You can provide your custom exceptionHandler while building SpringBootLambdaContainerHandler.
public class StreamLambdaHandler implements RequestStreamHandler {
private static SpringBootLambdaContainerHandler<AwsProxyRequest, AwsProxyResponse> handler;
static {
handler = new SpringBootProxyHandlerBuilder<AwsProxyRequest>()
.defaultProxy()
.exceptionHandler(***your customer handler here***)
// other methods are skipped
.buildAndInitialize();
}
}
If you are using spring cloud functions no need to use SpringBootLambdaContainerHandler, what you need to do is create a custom routing function and handle the exception thrown from your lambda function and return APIGatewayProxyResponseEvent. below shows how I achieved the desired result
public class CustomRoutingFunction implements Function<Message<?>, APIGatewayProxyResponseEvent> {
private final FunctionCatalog functionCatalog;
private final FunctionProperties functionProperties;
private final MessageRoutingCallback routingCallback;
public static final String DEFAULT_ROUTE_HANDLER = "defaultMessageRoutingHandler";
public CustomRoutingFunction(FunctionCatalog functionCatalog,
FunctionProperties functionProperties,
MessageRoutingCallback routingCallback) {
this.functionCatalog = functionCatalog;
this.functionProperties = functionProperties;
this.routingCallback = routingCallback;
}
#Override
public APIGatewayProxyResponseEvent apply(Message<?> input) {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
try {
String functionDefinition = this.routingCallback.routingResult(input);
SimpleFunctionRegistry.FunctionInvocationWrapper function = functionCatalog.lookup(functionDefinition);
Object output = function.apply(input);
String payload = mapper.writeValueAsString(output);
return new APIGatewayProxyResponseEvent()
.withIsBase64Encoded(false)
.withBody(payload)
.withHeaders(Map.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE,
"statusCode", "200"))
.withStatusCode(200);
} catch (Exception e) {
return new APIGatewayProxyResponseEvent()
.withIsBase64Encoded(false)
.withHeaders(Map.of(HttpHeaders.CONTENT_TYPE,
MediaType.APPLICATION_JSON_VALUE,
"statusCode", String.valueOf(HttpStatus.BAD_REQUEST.value())))
.withBody(e.getMessage())
.withStatusCode(HttpStatus.BAD_REQUEST.value());
}
}
}
now you need to register your router function as a bean and pass it to spring.cloud.function.definition
#Bean
public CustomRoutingFunction customRoutingFunction(FunctionCatalog functionCatalog,
FunctionProperties functionProperties,
#Nullable MessageRoutingCallback routingCallback,
#Nullable DefaultMessageRoutingHandler defaultMessageRoutingHandler) {
if (defaultMessageRoutingHandler != null) {
FunctionRegistration functionRegistration = new FunctionRegistration(defaultMessageRoutingHandler, CustomRoutingFunction.DEFAULT_ROUTE_HANDLER);
functionRegistration.type(FunctionTypeUtils.consumerType(ResolvableType.forClassWithGenerics(Message.class, Object.class).getType()));
((FunctionRegistry) functionCatalog).register(functionRegistration);
}
return new CustomRoutingFunction(functionCatalog, functionProperties, routingCallback);
}
inside your application.yml file
spring:
cloud:
function:
definition: customRoutingFunction

Jooq DataTypeException since Spring.Boot 2.4.x

I am getting a DataTypeException when retrieving data since the upgrade to Spring Boot 2.4.x. It worked fine with 2.3.9.RELEASE.
org.jooq.exception.DataTypeException: No Converter found for types MyBaseType and MyInheritType1 at
org.jooq.impl.Tools.converterOrFail(Tools.java:1132) at
org.jooq.impl.Tools.converterOrFail(Tools.java:1148) at
org.jooq.impl.AbstractRecord.get(AbstractRecord.java:270) at
org.jooq.impl.AbstractResultQuery.fetchOne(AbstractResultQuery.java:576) at
org.jooq.impl.SelectImpl.fetchOne(SelectImpl.java:3019)
MyInheritType1 extends MyBaseType.
The classes are using lombok #Data, so they should have the proper setters.
#Data
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_class")
#JsonSubTypes(
{
#JsonSubTypes.Type(value = MyInheritType1.class, name = "Type1"),
#JsonSubTypes.Type(value = MyInheritType2.class, name = "Type2")
})
public class MyBaseType
{
private UUID id;
private String disclaimerLongText = "";
private LocalDateTime creationTime;
private Map<UUID, String> images = new HashMap<>();
}
The inherited type:
#Data
public class MyInheritType1 extends MyBaseType
{
private String baseMap;
private EnumType someEnum;
private List<LayerType> layerTypes = new ArrayList<>();
private double[] center;
}
I retrieve the data like this:
return dsl.select(PROJECT.DETAILS).from(PROJECT)
.where(PROJECT.ID.eq(id.toString()))
.fetchOne(PROJECT.DETAILS, MyInheritType1.class);
PROJECT.DETAILS is defined as this:
public final TableField<ProjectRecord, ProjectDetails> DETAILS = createField(DSL.name("details"), SQLDataType.JSONB.nullable(false), this, "", new ProjectDetailsBinding());
And ProjectDetailsBinding looks like this:
public class ProjectDetailsBinding extends JsonBBinding<MyBaseType>
{
#Override
protected Class<ProjectDetails> getBindingType()
{
return MyBaseType.class;
}
}
public abstract class JsonBBinding<T> implements Binding<JSONB, T>
{
private ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule());
protected abstract Class<T> getBindingType();
#Override
public Converter<JSONB, T> converter()
{
return new Converter<JSONB, T>()
{
#Override
public T from(JSONB o)
{
if (o == null)
return null;
try
{
return objectMapper.readValue(o.data(), getBindingType());
} catch (Exception e)
{
e.printStackTrace();
}
return null;
}
#Override
public JSONB to(T t)
{
try
{
return JSONB.valueOf(objectMapper.writeValueAsString(t));
} catch (JsonProcessingException e)
{
e.printStackTrace();
}
return null;
}
#Override
public Class<JSONB> fromType()
{
return JSONB.class;
}
#Override
public Class<T> toType()
{
return getBindingType();
}
};
}
[..]
}
Since it worked with 2.3.9.RELEASE, I am wondering what changed in Spring Boot or Jooq, that would cause this different behavior now?
Looks like https://github.com/jOOQ/jOOQ/issues/11762, fixed for 3.15.0 and 3.14.9, to be released soon. You can try building 3.14.9 from github or use a snapshot build from here: https://www.jooq.org/download/versions if you're licensed, to see if that fixes your issue.
Alternatively, you can try to use the fixed version of the DefaultConverterProvider and use that in your Configuration.
Since it worked with 2.3.9.RELEASE, I am wondering what changed in Spring Boot or Jooq, that would cause this different behavior now?
Typically, Spring Boot upgrades come with jOOQ upgrades. You could also downgrade your jOOQ dependency to what you were using before with Spring Boot 2.3.9

Spring MVC Test with RestTemplate: Generic collection fails (even with ParameterizedTypeReference)

I am working with Spring Framework 4.3.1
I have the following domain class
#XmlRootElement(name="persona")
#XmlType(propOrder = {"id","nombre","apellido","fecha"})
public class Persona implements Serializable {
#XmlElement(name="id")
#JsonProperty("id")
public String getId() {
return id;
}
....
Where each getter has the #XmlElement and #JsonProperty annotations.
I am working with JAXB2 and Jackson2
I have the following too:
#XmlRootElement(name="collection")
public class GenericCollection<T> {
private Collection<T> collection;
public GenericCollection(){
}
public GenericCollection(Collection<T> collection){
this.collection = collection;
}
#XmlElement(name="item")
#JsonProperty("collection")
public Collection<T> getCollection() {
return collection;
}
public void setCollection(Collection<T> collection) {
this.collection = collection;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
for(Object object : collection){
builder.append("[");
builder.append(object.toString());
builder.append("]");
}
return builder.toString();
}
}
About Testing, the many #Tests methods working through Spring MVC Test work fine. The #Controller and #RestController work how is expected.
Note: I can test the CRUD scenarios, it about the HTTP methods such as POST, PUT, GET and DELETE. Therefore I am able to get one entity and a collection of entities.
Note: from the previous note, all works working around the XML and JSON formats.
Now trying to do testing through the RestTemplate how a kind of programmatic client, it only fails for collections. With the following:
#Before
public void setUp(){
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc));
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(httpMessageConverterConfig.marshallingMessageConverter());
converters.add(httpMessageConverterConfig.mappingJackson2HttpMessageConverter());
restTemplate.setMessageConverters(converters);
System.out.println("converters.size():" + converters.size());
}
I can confirm converters.size() always prints 2
The following is for XML and JSON
#Test
public void findAllXmlTest(){
RequestEntity<Void> requestEntity = RestControllerSupport_.createRequestEntityForGet(uri, retrieveURI);
ParameterizedTypeReference<GenericCollection<Persona>> parameterizedTypeReference = new ParameterizedTypeReference<GenericCollection<Persona>>(){};
ResponseEntity<GenericCollection<Persona>> responseEntity = restTemplate.exchange(requestEntity, parameterizedTypeReference);
assertThat(responseEntity, notNullValue());
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getHeaders().getContentType(), is(MediaType.APPLICATION_XML) );
assertThat(responseEntity.getBody(), notNullValue());
assertThat(responseEntity.getBody().getClass(), is(GenericCollection.class));
assertThat(responseEntity.getBody().getCollection(), is(personas));
}
#Test
public void findAllJsonTest(){
RequestEntity<Void> requestEntity = RestControllerSupport_.createRequestEntityForGet(uri, retrieveURI);
ParameterizedTypeReference<GenericCollection<Persona>> parameterizedTypeReference = new ParameterizedTypeReference<GenericCollection<Persona>>(){};
ResponseEntity<GenericCollection<Persona>> responseEntity = restTemplate.exchange(requestEntity, parameterizedTypeReference);
assertThat(responseEntity, notNullValue());
assertThat(responseEntity.getStatusCode(), is(HttpStatus.OK));
assertThat(responseEntity.getHeaders().getContentType(), is(MediaType.APPLICATION_JSON_UTF8) );
assertThat(responseEntity.getBody(), notNullValue());
assertThat(responseEntity.getBody().getClass(), is(GenericCollection.class));
assertThat(responseEntity.getBody().getCollection(), is(personas));
}
Note: observe I am using ParameterizedTypeReference for both scenarios.
For JSON it works.
But for XML I get:
org.springframework.web.client.RestClientException: Could not extract response: no suitable HttpMessageConverter found for response type [com.manuel.jordan.controller.support.GenericCollection<com.manuel.jordan.domain.Persona>] and content type [application/xml]
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:109)
What is wrong or missing?
Your problem that you use MarshallingHttpMessageConverter which isn't GenericHttpMessageConverter, like it is expected for the ParameterizedTypeReference in the HttpMessageConverterExtractor:
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericMessageConverter =
(GenericHttpMessageConverter<?>) messageConverter;
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
The MappingJackson2HttpMessageConverter is that one.
So, I suggest you to try with Jaxb2CollectionHttpMessageConverter.

How to make Spring MVC return CSV as convenient as return JSON

I know Spring MVC could return a model in format of Json easily; however, I have tried quite different approaches to return a model in CSV format (via Jackson), but could not make it successfully.
What should I do?
I enclose my model code, controller code, and gradle.build as following:
Thanks a lot!
Model:
#JsonPropertyOrder({ "staffName", "name" })
public class Greeter
{
String name;
String staffName[];
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public String[] getStaffName()
{
return staffName;
}
public void setStaffName(String[] staffName)
{
this.staffName = staffName;
}
}
Controller:
#Controller
public class GreetingController {
#RequestMapping(value = "/greeter/json", method = RequestMethod.GET)
public #ResponseBody
Greeter getGreeterInJSON() {
Greeter greeter = new Greeter();
greeter.setName("default");
greeter.setStaffName(new String[] { "ye", "lichi" });
return greeter;
}
#RequestMapping(value = "/greeter/csv", method = RequestMethod.GET, consumes = "text/csv")
public #ResponseBody
Greeter getGreeterInCSV(HttpServletResponse response) {
Greeter greeter = new Greeter();
greeter.setName("default");
greeter.setStaffName(new String[] { "ye", "lichi" });
CsvMapper mapper = new CsvMapper();
CsvSchema schema = mapper.schemaFor(Greeter.class);
ObjectWriter writer = mapper.writer(schema.withLineSeparator("\n"));
File greeterCSV = new File("greeterCSV.csv");
try {
writer.writeValue(greeterCSV, greeter);
} catch (JsonGenerationException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return greeter;
}
}
build.gradle dependencies:
dependencies {
compile group: 'commons-collections', name: 'commons-collections', version: '3.2'
compile ('org.springframework:spring-context:4.0.0.RELEASE')
compile("org.springframework.boot:spring-boot-starter-web:0.5.0.M6")
compile("org.thymeleaf:thymeleaf-spring3:2.0.17")
// compile "org.codehaus.jackson:jackson-mapper-asl:1.9.13"
compile 'com.fasterxml.jackson.core:jackson-databind:2.3.0'
compile 'com.fasterxml.jackson.core:jackson-core:2.3.0'
compile 'com.fasterxml.jackson.core:jackson-annotations:2.3.0'
compile 'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.3.0'
testCompile group: 'junit', name: 'junit', version: '4.+'
testCompile "org.mockito:mockito-all:1.9.5"
}
Edit:
Tomcat error:
HTTP Status 415 -
type Status report
message
description The server refused this request because the request entity is in a format not supported by the requested resource for the requested method.
You want not to consume but produce csv. The way you have it, the service expects the input to be provided in csv format, that's why it complains about 'request entity is in a format not supported'. Key is 'request' here - it expects some input in csv format.
Changing 'consume' to 'produces' should fix your problem.
I was looking for an answer to this question for a while and sadly found none, so I will give and example here.
You need to add a CSV message converter. This #Configuration does this:
#Configuration
public class CsvConfiguration {
#Bean
JacksonToCsvHttpMessageConverter csvMessageConverter() {
CsvMapper mapper = new CsvMapper();
// configure mapper here
mapper.registerModule(new JavaTimeModule());
return new JacksonToCsvHttpMessageConverter(mapper);
}
}
And a converter itself:
Note that this simplified version does not support filters nor views. However, if you study the overridden method writeInternal you should be able to add these features, should you need them. Additionally, empty collections are not supported, but you can use an empty array, if you want to support empty series.
public class JacksonToCsvHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
protected JacksonToCsvHttpMessageConverter(ObjectMapper objectMapper) {
super(objectMapper, MediaType.parseMediaType("text/csv"));
}
#Override
protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
if (object instanceof MappingJacksonValue) object = ((MappingJacksonValue) object).getValue();
CsvMapper mapper = (CsvMapper) getObjectMapper();
Class<?> klass = getType(object);
CsvSchema schema = mapper.schemaFor(klass).withHeader();
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
try (CsvGenerator generator = mapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) {
ObjectWriter writer = mapper.writer().with(schema);
writer.writeValue(generator, object);
}
}
private Class<?> getType(Object object) {
if (object instanceof Collection) {
Collection<?> c = (Collection<?>) object;
return c.iterator().next().getClass();
}
if (object.getClass().isArray()) return object.getClass().getComponentType();
return object.getClass();
}
}
This will enable all traditional controller methods to return CSV by the way of Accept header. Accept: application/json will yield JSON while Accept: text/csv will yield CSV. Of course all CsvMapper constraints will apply, so no nested objects etc.
#RestController
public class CsvController {
#GetMapping("/records*")
public CsvRecord[] getRecords() {
return new CsvRecord[] { new CsvRecord(), new CsvRecord() };
}
#GetMapping("/record")
public CsvRecord getRecord() {
return new CsvRecord();
}
#JsonAutoDetect(fieldVisibility = Visibility.PUBLIC_ONLY)
public static class CsvRecord {
public String name = "aa";
}
}
Using this configuration will cause your app to return CSV for Accept: */* requests. This is rarely intended, so you can set the default content type to JSON by adding this configuration:
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
you are using GET so you are expecting something in response which is CSV so make sure to use produces = "text/csv" instead of consumes = "text/csv")
here :-
#RequestMapping(value = "/greeter/csv", method = RequestMethod.GET, consumes = "text/csv")
public #ResponseBody
we use consume when we send some

Resources