BatchMessagingMessageConverter support headerMapper trusted types - spring-kafka

I'm using spring-boot 2.7.4 and spring-cloud-dependencies 2021.0.4.
I haven't found any solution in spring documentation for add trustedTypes in BatchMessagingMessageConverter. I'm using kafka for read messages in batch-mode. If I insert a custom header (my own class) when the consumer read the header return a DefaultKafkaHeaderMapper$NonTrustedHeaderType and not my class.
I have in my configuration this key to activate batch mode:
spring.cloud.stream.bindings.nameBind-in-0.consumer.batch-mode=true
I tried in debug to add to headerMapper in BatchMessagingMessageConverter the package of my class and all works fine. There is a way to specify my package in configuration?
I followed the documentation https://docs.spring.io/spring-cloud-stream/docs/3.2.5/reference/html/spring-cloud-stream-binder-kafka.html#kafka-binder-properties, I created a bean like this:
#Bean("kafkaHeaderMapperCustom")
KafkaHeaderMapper getKafkaHeaderMapperCustom() {
var defKHM = new DefaultKafkaHeaderMapper();
defKHM.addTrustedPackages("*");
return defKHM;
}
Specified to key spring.cloud.stream.kafka.binder.headerMapperBeanName in configuration but doesn't work, I suppose that configuration is valid for not batch context?
I tried also these properties:
spring.kafka.consumer.properties.spring.json.trusted.packages
spring.json.trusted.packages
EDIT - Add example:
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.function.StreamBridge;
import org.springframework.context.annotation.Bean;
import org.springframework.kafka.support.DefaultKafkaHeaderMapper;
import org.springframework.kafka.support.KafkaHeaderMapper;
import org.springframework.kafka.support.KafkaHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
#SpringBootApplication
public class Application {
public static final String HEADER_KEY = "CUSTOM_HEADER_KEY";
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public ApplicationRunner runner(StreamBridge streamBridge) {
return args -> {
var headers = new MessageHeaders(Map.of(HEADER_KEY, new CustomHeaderClass("field1Value", LocalDate.now())));
headers.get(KafkaHeaders.BATCH_CONVERTED_HEADERS);
var message = MessageBuilder.createMessage(new ExampleBrokenHeaderEntity("randomValue", "randomName"), headers);
streamBridge.send("stackoverflow-example", message);
};
}
#Bean
public Consumer<Message<List<ExampleBrokenHeaderEntity>>> readFromKafkaBatchMode() {
return messages -> {
var brokenHeader = ((ArrayList<Map<String, Object>>) messages.getHeaders().get(KafkaHeaders.BATCH_CONVERTED_HEADERS)).get(0).get(HEADER_KEY);
System.out.println("BATCH - Class header: " + (brokenHeader != null ? brokenHeader.getClass() : null));
};
}
#Bean
public Consumer<Message<ExampleBrokenHeaderEntity>> readFromKafkaNoBatchMode() {
return messages -> {
var brokenHeader = messages.getHeaders().get(HEADER_KEY);
System.out.println("NO_BATCH - Class header: " + (brokenHeader != null ? brokenHeader.getClass() : null));
};
}
#Bean("kafkaHeaderMapperCustom")
public KafkaHeaderMapper getKafkaHeaderMapperBatchMode() {
var kafkaHeaderMapperCustom = new DefaultKafkaHeaderMapper();
kafkaHeaderMapperCustom.addTrustedPackages("*");
return kafkaHeaderMapperCustom;
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
#Data
#NoArgsConstructor
#AllArgsConstructor
public class CustomHeaderClass {
private String field1;
private LocalDate field2;
}
import lombok.AllArgsConstructor;
import lombok.Data;
#Data
#AllArgsConstructor
public final class ExampleBrokenHeaderEntity {
private String type;
private String name;
}
spring.cloud.stream.kafka.binder.brokers=x.x.x.x:xxxx
spring.cloud.function.definition=readFromKafkaNoBatchMode;readFromKafkaBatchMode
spring.cloud.stream.bindings.readFromKafkaBatchMode-in-0.destination=stackoverflow-example
spring.cloud.stream.bindings.readFromKafkaBatchMode-in-0.group=readFromKafkaBatchMode
spring.cloud.stream.bindings.readFromKafkaBatchMode-in-0.consumer.batch-mode=true
spring.cloud.stream.bindings.readFromKafkaNoBatchMode-in-0.destination=stackoverflow-example
spring.cloud.stream.bindings.readFromKafkaNoBatchMode-in-0.group=readFromKafkaNoBatchMode
spring.cloud.stream.kafka.binder.headerMapperBeanName=kafkaHeaderMapperCustom
The output of example is:
NO_BATCH - Class header: class com.example.kafka.header.types.CustomHeaderClass
BATCH - Class header: class org.springframework.kafka.support.DefaultKafkaHeaderMapper$NonTrustedHeaderType

It's a bug; the binder only sets the custom header mapper on a record converter:
private MessageConverter getMessageConverter(
final ExtendedConsumerProperties<KafkaConsumerProperties> extendedConsumerProperties) {
MessageConverter messageConverter = BindingUtils.getConsumerMessageConverter(getApplicationContext(),
extendedConsumerProperties, this.configurationProperties);
if (messageConverter instanceof MessagingMessageConverter) {
((MessagingMessageConverter) messageConverter).setHeaderMapper(getHeaderMapper(extendedConsumerProperties));
}
return messageConverter;
}
There should be similar code for when the converter is a BatchMessagingMessageConverter.
The work around is to define a custom message converter for the batch consumer:
#Bean("batchConverter")
BatchMessageConverter batchConverter(KafkaHeaderMapper kafkaHeaderMapperCustom) {
BatchMessagingMessageConverter batchConv = new BatchMessagingMessageConverter();
batchConv.setHeaderMapper(kafkaHeaderMapperCustom);
return batchConv;
}
spring.cloud.stream.kafka.bindings.readFromKafkaBatchMode-in-0.consumer.converter-bean-name=batchConverter
NO_BATCH - Class header: class com.example.demo.So74294156Application$CustomHeaderClass
BATCH - Class header: class com.example.demo.So74294156Application$CustomHeaderClass
Please open an issue against Spring Cloud Stream, referencing this question/answer.

Related

Test a Reactive-Kafka Consumer and Producer Template using embedded kafka + custom serialised

We need an example on how to test ReactiveKafkaConsumerTemplate and ReactiveKafkaProducerTemplate with an embedded-kafka-broker. Thanks.
CORRECT CODE IS HERE AFTR DISCUSSION
You can have your custom de-serializer accordingly to use custom ReactiveKafkaConsumerTemplate
Custom Serialiser:
import org.apache.kafka.common.serialization.Serializer;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class EmployeeSerializer implements Serializer<Employee> {
#Override
public byte[] serialize(String topic, Employee data) {
byte[] rb = null;
ObjectMapper mapper = new ObjectMapper();
try {
rb = mapper.writeValueAsString(data).getBytes();
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return rb;
}
}
Use it part of embedded-kfka-reactive test:
import java.util.Map;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.connect.json.JsonSerializer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
import org.springframework.kafka.support.converter.MessagingMessageConverter;
import org.springframework.kafka.test.condition.EmbeddedKafkaCondition;
import org.springframework.kafka.test.context.EmbeddedKafka;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import reactor.kafka.sender.SenderOptions;
import reactor.kafka.sender.SenderRecord;
import reactor.test.StepVerifier;
#EmbeddedKafka(topics = EmbeddedKafkareactiveTest.REACTIVE_INT_KEY_TOPIC,
brokerProperties = { "transaction.state.log.replication.factor=1", "transaction.state.log.min.isr=1" })
public class EmbeddedKafkareactiveTest {
public static final String REACTIVE_INT_KEY_TOPIC = "reactive_int_key_topic";
private static final Integer DEFAULT_KEY = 1;
private static final String DEFAULT_VERIFY_TIMEOUT = null;
private ReactiveKafkaProducerTemplate<Integer, Employee> reactiveKafkaProducerTemplate;
#BeforeEach
public void setUp() {
reactiveKafkaProducerTemplate = new ReactiveKafkaProducerTemplate<>(setupSenderOptionsWithDefaultTopic(),
new MessagingMessageConverter());
}
private SenderOptions<Integer, Employee> setupSenderOptionsWithDefaultTopic() {
Map<String, Object> senderProps = KafkaTestUtils
.producerProps(EmbeddedKafkaCondition.getBroker().getBrokersAsString());
SenderOptions<Integer, Employee> senderOptions = SenderOptions.create(senderProps);
senderOptions = senderOptions.producerProperty(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "reactive.transaction")
.producerProperty(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true)
.producerProperty(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class.getName())
;
return senderOptions;
}
#Test
public void test_When_Publish() {
Employee employee = new Employee();
ProducerRecord<Integer, Employee> producerRecord = new ProducerRecord<Integer, Employee>(REACTIVE_INT_KEY_TOPIC, DEFAULT_KEY, employee);
StepVerifier.create(reactiveKafkaProducerTemplate.send(producerRecord)
.then())
.expectComplete()
.verify();
}
#AfterEach
public void tearDown() {
reactiveKafkaProducerTemplate.close();
}
}
The tests in the framework use an embedded kafka broker.
https://github.com/spring-projects/spring-kafka/tree/main/spring-kafka/src/test/java/org/springframework/kafka/core/reactive
#EmbeddedKafka(topics = ReactiveKafkaProducerTemplateIntegrationTests.REACTIVE_INT_KEY_TOPIC, partitions = 2)
public class ReactiveKafkaProducerTemplateIntegrationTests {
...
added correct serialised with a non-transactional producer. please see the code on top of this page for the answer.

Spring webflux - ServerWebExchangeDecorator code is not executed when an exception is thrown

I have a Webflux application, where I have a ServerWebExchangeDecorator that decorates the request and responses. I have overrides to do some logging and then call the super methods.
This is what I have in code:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
#Component
public class LoggingWebFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(decorate(exchange));
}
private ServerWebExchange decorate(ServerWebExchange exchange) {
final ServerHttpRequest decoratedRequest = new LoggingServerHttpRequestDecorator(exchange.getRequest());
final ServerHttpResponse decoratedResponse = new LoggingServerHttpResponseDecorator(exchange.getResponse());
return new ServerWebExchangeDecorator(exchange) {
#Override
public ServerHttpRequest getRequest() {
return decoratedRequest;
}
#Override
public ServerHttpResponse getResponse() {
return decoratedResponse;
}
};
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import reactor.core.publisher.Flux;
public class LoggingServerHttpRequestDecorator extends ServerHttpRequestDecorator {
private static final Logger logger = LoggerFactory.getLogger(LoggingServerHttpRequestDecorator.class);
public LoggingServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
}
#Override
public Flux<DataBuffer> getBody() {
logger.info("getBody method");
return super.getBody();
}
}
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import reactor.core.publisher.Mono;
public class LoggingServerHttpResponseDecorator extends ServerHttpResponseDecorator {
private static final Logger logger = LoggerFactory.getLogger(LoggingServerHttpResponseDecorator.class);
public LoggingServerHttpResponseDecorator(ServerHttpResponse delegate) {
super(delegate);
}
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
logger.info("writeWith method");//THIS LINE IS NOT EXECUTED WHEN AN EXCEPTION IS THROWN
return super.writeWith(body);
}
#Override
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
logger.info("writeAndFlushWith method");
return super.writeAndFlushWith(body);
}
}
When I do a happy path with a POST request, this works fine, but when an exception is thrown, the Response Decorator is omitted and my custom code is not being executed.
This is a controller code to replicate the issue:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
#RestController
#RequestMapping("/decorator-demo")
public class DecoratorDemoController {
/** The Constant logger. */
private static final Logger logger = LoggerFactory.getLogger(DecoratorDemoController.class);
#PostMapping(produces = MediaType.APPLICATION_STREAM_JSON_VALUE, consumes = MediaType.APPLICATION_STREAM_JSON_VALUE)
public Mono<ResponseEntity<String>> postData(#RequestBody String id) {
logger.info("attempting to post the data");
if(id.length() == 1){
Mono<String> created = Mono.just(id);
return created.flatMap(vo -> Mono.just(ResponseEntity.status(HttpStatus.CREATED).body(vo)));
}
throw new IllegalArgumentException("String length must be 1");
}
}
When I post a single character, I have the logs I am expecting:
LoggingServerHttpRequestDecorator : getBody method
DecoratorDemoController : attempting to post the data
LoggingServerHttpResponseDecorator : writeWith method
But when I post more than one character, this is the logs I am having:
LoggingServerHttpRequestDecorator : getBody method
DecoratorDemoController : attempting to post the data
AbstractErrorWebExceptionHandler : [0b933716] 500 Server Error for HTTP POST "/decorator-demo"
Am I doing something wrong, or missing something?
Try this
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
#Component
public class ResponseInterceptorFilter implements Ordered, GlobalFilter {
private static final Logger log = LoggerFactory.getLogger(ResponseInterceptorFilter.class);
public ResponseInterceptorFilter(SqsPublisher sqsPublisher) {
this.sqsPublisher = sqsPublisher;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory dataBufferFactory = response.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (!(body instanceof Flux)) {
return super.writeWith(body);
}
Flux<? extends DataBuffer> flux = (Flux<? extends DataBuffer>) body;
return super.writeWith(flux.buffer().map(dataBuffers -> {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
dataBuffers.forEach(buffer -> {
// three byte copies here
byte[] array = new byte[buffer.readableByteCount()];
buffer.read(array);
try {
outputStream.write(array);
} catch (IOException e) {
// TODO: need catch?
}
DataBufferUtils.release(buffer);
});
byte[] write = outputStream.toByteArray();
String responseBody = new String(write);
log.debug("Response ----> {}", responseBody);
response.getHeaders().setContentLength(write.length);
return dataBufferFactory.wrap(write);
}));
}
};
ServerWebExchange serverWebExchange = exchange.mutate().response(decoratedResponse).build();
return chain.filter(serverWebExchange);
}
#Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1; // this is important
}
}

Java Code to invoke PATCH method from JUnit

I have a simple MyLibraryApplication which is having code to invoke POST(TransactionControllerImpl.issueBookToMember) and PATCH(TransactionControllerImpl.returnBookTransaction) methods. I have referred some links on net and tried my best to write code to invoke PATCH method. The code can be found in TransactionControllerTest(testBookReturnUsingRestTemplate and testBookReturnUsingMockMvc methods). The code for invoking POST is working fine but the code for invoking PATCH is not working. Control never reaches returnBookTransaction inside TransactionControllerImpl.
Error: Invalid PATCH method.
I am looking for code snippet for TransactionControllerTest.testBookReturnUsingRestTemplate and testBookReturnUsingMockMvc methods. Can someone help me in getting this code into proper shape?
package com.mycompany.techtrial;
import java.util.Map;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import com.mycompany.techtrial.Transaction;
public interface TransactionController {
public ResponseEntity<Transaction> issueBookToMember(#RequestBody Map<String, String> params);
public ResponseEntity<Transaction> returnBookTransaction(#PathVariable(name="transaction-id") Long transactionId);
}
/**
*
*/
package com.mycompany.techtrial;
import java.time.LocalDateTime;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class TransactionControllerImpl implements TransactionController{
/*
* PLEASE DO NOT CHANGE SIGNATURE OR METHOD TYPE OF END POINTS
* Example Post Request : { "book":"Java8 Primer","member":"Test 1" }
*/
#PostMapping(path = "/api/transaction")
public ResponseEntity<Transaction> issueBookToMember(#RequestBody Map<String, String> params){
String book = params.get("book");
String member = params.get("member");
Transaction transaction = new Transaction();
transaction.setId(1L);
transaction.setBook(book);
transaction.setMember(member);
transaction.setDateOfIssue(LocalDateTime.now());
transaction.setDateOfReturn(Transaction.getDefaultReturnDate());
return ResponseEntity.ok().body(transaction);
}
/*
* PLEASE DO NOT CHANGE SIGNATURE OR METHOD TYPE OF END POINTS
*/
#PatchMapping(path= "/api/transaction/{transaction-id}/return")
public ResponseEntity<Transaction> returnBookTransaction(#PathVariable(name="transaction-id") Long transactionId){
String book = "Java8 Primer";
String member = "Test 1";
Transaction transaction = new Transaction();
transaction.setId(1L);
transaction.setBook(book);
transaction.setMember(member);
transaction.setDateOfIssue(LocalDateTime.now().minusDays(10));
transaction.setDateOfReturn(LocalDateTime.now());
return ResponseEntity.ok().body(transaction);
}
}
package com.mycompany.techtrial;
import java.util.HashMap;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
#RunWith(SpringJUnit4ClassRunner.class)
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class TransactionControllerTest {
MockMvc mockMvc;
#Mock
private TransactionController transactionController;
#Autowired
private TestRestTemplate template;
#Before
public void setup() throws Exception {
mockMvc = MockMvcBuilders.standaloneSetup(transactionController).build();
}
#Test
public void testBookIssue() throws Exception {
HttpEntity<Object> transaction = getHttpEntity(
"{\"book\": \"Java8 Primer\", \"member\": \"Test 1\" }");
ResponseEntity<Transaction> response = template.postForEntity(
"/api/transaction", transaction, Transaction.class);
Assert.assertEquals("Java8 Primer", response.getBody().getBook());
Assert.assertEquals("Test 1", response.getBody().getMember());
Assert.assertEquals(200,response.getStatusCode().value());
}
#Test
public void testBookReturnUsingRestTemplate() throws Exception {
Long transactionId = new Long(1);
HashMap<String,Long> uriVariables = new HashMap<String,Long>();
uriVariables.put("transaction-id", transactionId);
Transaction transaction = template.patchForObject(
"/api/transaction/{transaction-id}/return",null, Transaction.class, uriVariables);
Assert.assertEquals(new Long(1), transaction.getId());
//Assert.assertEquals(200,response.getStatusCode().value());
}
#Test
public void testBookReturnUsingMockMvc() throws Exception {
Long transactionId = new Long(1);
HashMap<String,Long> uriVariables = new HashMap<String,Long>();
uriVariables.put("transaction-id", transactionId);
ResultActions obj = mockMvc.perform( MockMvcRequestBuilders
.patch("/api/transaction/{transaction-id}/return",transactionId)
.content("")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON));
System.out.println(obj.getClass());
HttpStatus status = obj.andReturn().getModelAndView().getStatus();
boolean success = status.is2xxSuccessful();
System.out.println("success="+success);
Assert.assertEquals(new Long(1), transactionId);
//Assert.assertEquals(200,response.getStatusCode().value());
}
private HttpEntity<Object> getHttpEntity(Object body) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new HttpEntity<Object>(body, headers);
}
}
package com.mycompany.techtrial;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class Transaction implements Serializable {
private static final long serialVersionUID = 8951221480021840448L;
private static final LocalDateTime defaultReturnDate = LocalDateTime.of(LocalDate.of(2299, 12, 31), LocalTime.of(12, 0, 0));
Long id;
private String book;
private String member;
public String getBook() {
return book;
}
public void setBook(String book) {
this.book = book;
}
public String getMember() {
return member;
}
public void setMember(String member) {
this.member = member;
}
//Date and time of issuance of this book
LocalDateTime dateOfIssue;
//Date and time of return of this book
LocalDateTime dateOfReturn;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public LocalDateTime getDateOfIssue() {
return dateOfIssue;
}
public void setDateOfIssue(LocalDateTime dateOfIssue) {
this.dateOfIssue = dateOfIssue;
}
public LocalDateTime getDateOfReturn() {
return dateOfReturn;
}
public void setDateOfReturn(LocalDateTime dateOfReturn) {
this.dateOfReturn = dateOfReturn;
}
#Override
public String toString() {
return "Transaction [id=" + id + ", book=" + book + ", member=" + member + ", dateOfIssue=" + dateOfIssue + ", dateOfReturn=" + dateOfReturn + "]";
}
//#PrePersist
void preInsert() {
if (this.dateOfReturn == null)
this.dateOfReturn = defaultReturnDate;
}
public static LocalDateTime getDefaultReturnDate() {
return defaultReturnDate;
}
}
package com.mycompany.techtrial;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
#SpringBootApplication
public class MyLibraryApplication {
public static void main(String[] args) {
SpringApplication.run(MyLibraryApplication.class, args);
}
}
It seems to be a known issue with the RestTemplate default Http client.
RestTemplate bug
A workaround for this would be to use the apache httpcomponents httpclient library in the RestTemplateBuilder.setRequestFactory and pass that in the constructor to TestRestTemplate
After that you can use the exchange method on the TestRestTemplate class and do a PATCH request.
Sample code to create TestRestTemplate:
Supplier<ClientHttpRequestFactory> supplier = () -> {
return new HttpComponentsClientHttpRequestFactory();
};
restTemplateBuilder.requestFactory(supplier);
TestRestTemplate testRestTemplate = new TestRestTemplate(restTemplateBuilder);
testRestTemplate.exchange("/api/transaction/{transaction-id}/return",HttpMethod.PATCH,null,Transaction.class,uriVariables);

How to resolve view in thymleaf + Springboot?

Currently, i have function, which is to convert the data from MYSQL to CSV. The CSV function contain the webconfig where use the viewResolver. The problem is, when i used below function, the page cannot view but the CSV file can be download and vice versa. Is there anything that i need to configure ?
-Configure ContentNegotiatingViewResolver
#Bean
public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(manager);
// Define all possible view resolvers
List<ViewResolver> resolvers = new ArrayList<>();
resolvers.add(csvViewResolver());
resolver.setViewResolvers(resolvers);
return resolver;
}
WebConfig- full code
package com.portal.dmtt.csvDownload.config;
import com.portal.dmtt.csvDownload.viewResolver.CsvViewResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import java.util.ArrayList;
import java.util.List;
#Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.defaultContentType(MediaType.APPLICATION_JSON)
.favorPathExtension(true);
}
/*
* Configure ContentNegotiatingViewResolver
*/
#Bean
public ViewResolver contentNegotiatingViewResolver(ContentNegotiationManager manager) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(manager);
// Define all possible view resolvers
List<ViewResolver> resolvers = new ArrayList<>();
resolvers.add(csvViewResolver());
resolver.setViewResolvers(resolvers);
return resolver;
}
/*
* Configure View resolver to provide Csv output using Super Csv library to
* generate Csv output for an object content
*/
#Bean
public ViewResolver csvViewResolver() {
return new CsvViewResolver();
}
}
Export Controller
package com.portal.dmtt.csvDownload.controller;
import com.portal.dmtt.repo.dmttDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
#Controller
public class ExportController {
#Autowired
private dmttDAO dmttDAO;
/**
* Handle request to download an Excel document
*/
#GetMapping("/dl")
public String download(Model model) {
model.addAttribute("results", dmttDAO.getAllResultSet());
return "";
}
}
Abstract View
package com.portal.dmtt.csvDownload.view;
import org.springframework.web.servlet.view.AbstractView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
public abstract class AbstractCsvView extends AbstractView {
private static final String CONTENT_TYPE = "text/csv";
public AbstractCsvView() {
setContentType(CONTENT_TYPE);
}
#Override
protected boolean generatesDownloadContent() {
return true;
}
#Override
protected final void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
response.setContentType(getContentType());
buildCsvDocument(model, request, response);
}
protected abstract void buildCsvDocument(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response)
throws Exception;
}
CSV View
package com.portal.dmtt.csvDownload.view;
import com.portal.dmtt.model.exceptionMonitoring.FN_Result_Set;
import org.supercsv.io.CsvBeanWriter;
import org.supercsv.io.ICsvBeanWriter;
import org.supercsv.prefs.CsvPreference;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
public class CsvView extends AbstractCsvView {
#Override
protected void buildCsvDocument(Map<String, Object> model, HttpServletRequest request, HttpServletResponse
response) throws Exception {
response.setHeader("Content-Disposition", "attachment; filename=\"my-csv-file.csv\"");
List<FN_Result_Set> fnResultSetList = (List<FN_Result_Set>) model.get("results");
String[] header = {"SP_ID", "SP_ID", "XFER_XMIT_STATUS", "XFER_FILE_NAME", "UPDATE_TS", "YYMM", "REMARKS"};
try {
ICsvBeanWriter csvWriter = new CsvBeanWriter(response.getWriter(),
CsvPreference.STANDARD_PREFERENCE);
csvWriter.writeHeader(header);
for (FN_Result_Set user : fnResultSetList) {
csvWriter.write(user, header);
}
csvWriter.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
View Resolver
package com.portal.dmtt.csvDownload.viewResolver;
import com.portal.dmtt.csvDownload.view.CsvView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import java.util.Locale;
public class CsvViewResolver implements ViewResolver {
#Override
public View resolveViewName(String s, Locale locale) throws Exception {
return new CsvView();
}
}
One of the problems is that your CSVViewResolver is resolving a view for any view name. You may want to return null from CSVViewResolver.resolveViewName() if s, the view name, is not empty .
Another issue is that the browser (at least my Chrome) doesn't send text/csv as Accept header, but text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Set the media type explicitly in configureContentNegotiation for CSV:
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.defaultContentType(MediaType.APPLICATION_JSON)
.favorPathExtension(true)
.mediaType("csv", MediaType.parseMediaType("text/csv"));
}
Remove the Bean contentNegotiatingViewResolver
You shouldn't create a contentNegotiatingViewResolver as one is provided by Spring Boot. If you provide one, you will have two of this type, and yours will not have the Thymeleaf ViewResolver. As your CSVViewResolver will return a view for any view name, the view will be resolved in the ContentNegotiatingViewResolver provided by you, not in the one provided by Spring.
Details:
The csvViewResolver bean will be picked up by the Spring Boot's ContentNegotiatingViewResolver along others like BeanNameViewResolver, ThymeleafViewResolver, ViewResolverComposite, InternalResourceViewResolver.
To debug this set a breakpoint on DispatcherServlet.resolveViewName:
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}

Trouble with BeanValidation

I am trying to follow the example in https://jersey.java.net/documentation/latest/bean-validation.html#d0e13678, esp. section 18.4.3. I want to disallow json where Foo.number is missing. But my test returns status 200. Here is the full text of my test (I do have the jersey-bean-validation artifact in my pom):
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.glassfish.jersey.client.ClientConfig;
import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
public class DeleteMe extends JerseyTest {
#Path("/")
public static class Resource {
#POST
#Produces("application/json")
public Foo post(#Valid Foo foo) {
return foo;
}
}
public static class Foo {
#NotNull
private Integer number;
public void setNumber(final Integer number) {
this.number = number;
}
public Integer getNumber() {
return number;
}
}
#Override
protected Application configure() {
return new ResourceConfig(Resource.class)
.register(JacksonFeature.class);
}
#Override
protected void configureClient(final ClientConfig config) {
config.register(JacksonFeature.class);
}
#Test
public void testEntityFail() throws Exception {
Foo foo = new Foo();
Response response = target().request().post(javax.ws.rs.client.Entity.json(foo));
//I get 200 here...but foo is no good
assertTrue("Status "+response.getStatus(),response.getStatus() == 400);
}
}
PEBKAC -- I will leave the question up in case someone is getting started with validation and needs a dumbed-down "Hello World" test

Resources