I have this endpoint for Spring Rest API:
#PostMapping(value = "/v1/", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(#RequestBody PaymentTransaction transaction, HttpServletRequest request) throws Exception {
// get here plain XML
}
XML model.
#XmlRootElement(name = "payment_transaction")
#XmlAccessorType(XmlAccessType.FIELD)
public class PaymentTransaction {
public enum Response {
failed_response, successful_response
}
#XmlElement(name = "transaction_type")
public String transactionType;
.........
}
How I can get the XML request in plain XML text?
I also tried with Spring interceptor:
I tried this code:
#SpringBootApplication
#EntityScan("org.plugin.entity")
public class Application extends SpringBootServletInitializer implements WebMvcConfigurer {
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
........
#Bean
public RestTemplate rsestTemplate() {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
RestTemplate restTemplate = new RestTemplate(
new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
Component for logging:
#Component
public class RestTemplateHeaderModifierInterceptor implements ClientHttpRequestInterceptor {
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
StringBuilder sb = new StringBuilder();
sb.append("[ ");
for (byte b : body) {
sb.append(String.format("0x%02X ", b));
}
sb.append("]");
System.out.println("!!!!!!!!!!!!!!!");
System.out.println(sb.toString());
ClientHttpResponse response = execution.execute(request, body);
InputStream inputStream = response.getBody();
String result = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
System.out.println("!!!!!!!!!!!!!!!");
System.out.println(result);
return response;
}
}
But nothing is printed into the console. Any idea where I'm wrong? Probably this component is not registered?
Shouldn't it be easy like below to get it from HttpServletRequest, unless I'm missing something. I don't think there is need to use interceptor etc.
#PostMapping(value = "/v1/", consumes = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE,
MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(HttpServletRequest request) throws Exception {
String str, wholeXML = "";
try {
BufferedReader br = request.getReader();
while ((str = br.readLine()) != null) {
wholeXML += str;
}
System.out.println(wholeXML);
//Here goes comment question, to convert it into PaymentTransaction
JAXBContext jaxbContext = JAXBContext.newInstance(PaymentTransaction.class);
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
StringReader reader = new StringReader(wholeXML);
PaymentTransaction paymentTransaction = (PaymentTransaction) unmarshaller.unmarshal(reader);
}
We had the same issue and use this solution in production. Which is not framework dependent (always an upside in my book) and simple.
Just consume it without specifying it as an XML. Then read the request lines and join them by \n if you want to have new lines in your xml. If not, join them by "" or whatever you please. This presumes you are using the javax.servlet.http.HttpServletRequest
Example:
#PostMapping(value = "/v1")
public PaymentResponse handleMessage(HttpServletRequest request) throws Exception {
final InputStream xml = request.getInputStream();
final String xmlString = new BufferedReader(new InputStreamReader(xml))
.lines()
.collect(Collectors.joining("\n"));
// do whatever you please with it
}
And you have an plain xml string.
For your controller to receive the request body as a plain xml string, you need only change the #RequestBody parameter type to String:
#PostMapping(value = "/v1/", consumes = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE }, produces = { MediaType.APPLICATION_XML_VALUE, MediaType.APPLICATION_JSON_VALUE })
public PaymentResponse handleMessage(#RequestBody String xmlOrJson, HttpServletRequest request) throws Exception {
...
With the above mapping, if the client has submitted xml, you'll see the raw XML. Otherwise, if the client has submitted json, you'll see the raw JSON. Make sure you check the request's "Content-Type" header to know which type you're dealing with.
See https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-requestbody
We've been using the spring-mvc-logger in production for quite a while. It's written as a servlet filter, so can be added as an independent wrapper to the MVC endpoint.
Our set up is almost exactly like described on the readme.md there, though we restrict the <url-pattern> under the <filter-mapping> to just the useful endpoints.
Even if it's not exactly what you're after, the codebase there makes quite a nice small example. In particular note the request/response wrapping that is needed in the filter. (This is to avoid the IllegalStateException: getReader(), getInputStream() already called that would otherwise happen if getReader() were called twice).
You have created List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>(); but did not add the RestTemplateHeaderModifierInterceptor object to it.
You can autowire in the same in Application like below:
#Autowired
ClientHttpRequestInterceptor clientHttpRequestInterceptor;
and
interceptors.add(clientHttpRequestInterceptor);
The code looks like below:
class Application {
...
#Autowired
ClientHttpRequestInterceptor clientHttpRequestInterceptor;
#Bean
public RestTemplate rsestTemplate() {
List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
RestTemplate restTemplate = new RestTemplate(
new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()));
interceptors.add(clientHttpRequestInterceptor);
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
...
}
Hope it helps
Related
I'm new to MockMVC. I've successfully written some basic tests, but I got stuck on trying to test an use case with the endpoint that requires a POST request with two parameters - a POJO and an array of MultipartFile. The test is written as such:
#Test
public void vytvorPodnetTest() throws Exception {
var somePojo = new SomePojo();
somePojo.setSomeVariable("test_value");
var roles = List.of("TEST_USER");
var uid = "00000000-0000-0000-0000-000000000001";
MockMultipartFile[] attachments = {new MockMultipartFile("file1.txt", "file1.txt", "text/plain", "file1 content".getBytes()),
new MockMultipartFile("file2.txt", "file2.txt", "text/plain", "file2 content".getBytes())};
MockMultipartHttpServletRequestBuilder builder = MockMvcRequestBuilders.multipart("/some-pojo/create");
builder.with(req - {
req.setMethod("POST");
return req;
});
MvcResult result = mockMvc.perform(builder.file(attachments[0]).file(attachments[1])
.param("SomePojo", new ObjectMapper().writeValueAsString(somePojo))
.file(attachment[0])
.with(TestUtils.generateJWTToken(uid, roles)))
.andExpect(status.isOk())
.andReturn();
}
The controller method is as follows:
#PostMapping(value = "/create", consumes = {MediaType.MULTIPART_FORM_DATA_VALUE})
public UUID createPojo(
#RequestPart(value = "SomePojo") SomePojo somePojo,
#RequestPart(value = "attachments", required = false) MultipartFile[] attachments) {
return pojoService.create(somePojo, attachments);
}
It stops here, before reaching the service. I've tried adding the files both as a param "attachments" and like shown above, but all I get is "400 Bad Request"
Finally found the way to send the parameters as MockMultipartFile from MockMVC to the controller:
MockMultipartFile pojoJson = new MockMultipartFile("SomePojo", null,
"application/json", JsonUtils.toJSON(podnet).getBytes());
mockMvc.perform(MockMvcRequestBuilders.multipart("/some-pojo/create")
.file(pojoJson)
.contentType(MediaType.MULTIPART_FORM_DATA_VALUE)
.with(new TestUtils().generateJWTToken(uid, roles)))
.andExpect(status().isOk()).andReturn().getResponse().getContentAsString();
Please have a look at my codes below. The Java codes seemed to work just fine, but localhost:8080 gives me the error code 404 when I try to access it. I want to make localhost 8080 work. Please let me know if you need further information.
Application
#SpringBootApplication(exclude = { ErrorMvcAutoConfiguration.class })
// exclude part is to elimnate whitelabel error
#EnableScheduling
public class Covid19TrackerApplication {
public static void main(String[] args) {
SpringApplication.run(Covid19TrackerApplication.class, args);
}
}
Controller
#Controller
public class HomeController {
CovidDataService covidDataService;
#RequestMapping("/")
public #ResponseBody String home(Model model) {
model.addAttribute( "locationStats", covidDataService.getAllStats());
return "home";
}
}
Main Code
#Service
public class CovidDataService {
private static String Covid_Data_URL = "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_confirmed_global.csv";
private List<LocationStats> allStats = new ArrayList<>();
public List<LocationStats> getAllStats() {
return allStats;
}
#PostConstruct//?
#Scheduled(cron = "* * 1 * * *") //????
// * sec * min *hour and so on
public void fetchCovidData() throws IOException, InterruptedException {
List<LocationStats> newStats = new ArrayList<>(); // why we are adding this? To prevent user get an error while we are working on new data.
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(Covid_Data_URL))
.build(); // uri = uniform resource identifier
HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
StringReader csvBodyReader = new StringReader(httpResponse.body()); //StringReader needs to be imported
Iterable<CSVRecord> records = CSVFormat.DEFAULT.withFirstRecordAsHeader().parse(csvBodyReader); // parse(in) had error, we needed a "reader" instance.
for (CSVRecord record : records) {
LocationStats locationStat = new LocationStats(); //create an instance
locationStat.setState(record.get("Province/State"));
locationStat.setCountry(record.get("Country/Region"));
locationStat.setLatestTotalCase(Integer.parseInt(record.get(record.size()-1)));
System.out.println(locationStat);
newStats.add(locationStat);
}
this.allStats = newStats;
}
}
The problem may come from this piece of code
#RequestMapping("/")
public #ResponseBody String home(Model model) {
model.addAttribute( "locationStats", covidDataService.getAllStats());
return "home";
}
it returns "home" which should be existing view, normally, the view will be a jsp file which is placed somewhere in WEB-INF, please see this tutorial: https://www.baeldung.com/spring-mvc-view-resolver-tutorial
In the case of wrong mapping, it may returns 404 error
when you run the server, you should be able to see which port it's taken in the console.
Also, is server.port=8080 in the src/main/resources/application.properties file?
In the controller, the RequestMapping annotation is missing the method type and header
#RequestMapping(
path="/",
method= RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
public String home(Model model) {
model.addAttribute( "locationStats", covidDataService.getAllStats());
return "home";
}
make sure to add consumes for POST or PUT methods
A bit unrelated to the question but the line in the controller is missing #Autowired annotation
CovidDataService covidDataService;
Preferrably, add the #Autowired in the constructor
#Autowired
public HomeController(CovidDataService covidDataService) {
this.covidDataService = covidDataService;
}
I have some webservice endpoints that should offer json data by default. Therefore configuring as follows:
#Configuration
public class ContentNegotiationConfiguration implements WebMvcConfigurer {
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}
Problem: now I want to create an endpoint that offers a file download (thus is not json).
#RestController
public class FileServlet {
#GetMapping(value = "/docs/{filename}", consumes = MediaType.ALL_VALUE, produces = APPLICATION_OCTET_STREAM_VALUE)
public Object download(#Pathvariable filename) {
File file = fileservice.resolve(filename);
return new FileSystemResource(file);
}
}
Accessing this endpoint from the browser works fine. I can download the files.
But: when using native clients that are not setting any http headers like content-type, accept-header etc, the access fails with:
WARN o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver: Resolved
[org.springframework.web.HttpMediaTypeNotAcceptableException:
Could not find acceptable representation]
All of them result in the exception:
curl localhost:8080/docs/testfile.txt
curl -O localhost:8080/docs/testfile.txt
wget localhost:8080/docs/testfile.txt
This is probably because I set the default content type to json above in ContentNegotiationConfiguration. I cannot change that due to all the other endpoints that should be json by default.
Question: how can I explicit ignore that default json setting on that single endpoint, and always just offer the download stream?
Try custom ContentNegotiationStrategy with AntPathMatcher something like:
#Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// configurer.defaultContentType(MediaType.APPLICATION_JSON,MediaType.APPLICATION_OCTET_STREAM);
configurer.defaultContentTypeStrategy(
new ContentNegotiationStrategy() {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
AntPathMatcher antPathMatcher = new AntPathMatcher();
#Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
return null;
}
String path = this.urlPathHelper.getLookupPathForRequest(request);
if (antPathMatcher.match("/docs/*", path)) {
return Collections.singletonList(MediaType.APPLICATION_OCTET_STREAM);
} else {
return Collections.singletonList(MediaType.APPLICATION_JSON);
}
}
});
}
With the hint from #M. Deinum, I got it working as follows:
#GetMapping(value = "/docs/{filename}")
public void download(#Pathvariable filename) {
FileSystemResource file = new FileSystemResource(fileservice.resolve(filename));
rsp.setHeader("Content-Disposition", "attachment; filename=" + file.getFilename());
ResourceHttpMessageConverter handler = new ResourceHttpMessageConverter();
handler.write(file, MediaType.APPLICATION_OCTET_STREAM, new ServletServerHttpResponse(rsp));
}
That way writing directly to the stream bypassing the content negotiation, while still relying on the Spring class ResourceHttpMessageConverter for not having to implement the response writer myself.
I'm experiencing a very low serialization performance for a custom #RepositoryRestController method returning PagedResources<PersistentEntityResource>, e.g. I'm getting 15s serialization times for 2.5Mb JSON data instead of 0.5s after the workaround I made (more on it later).
Consider this:
#Entity
public class Content {
#OneToMany
private Set<ContentMapping> contentMappings = new HashSet<>();
// ...
}
#RepositoryRestController
public class MyController {
private final ContentService contentService;
private final PagedResourcesAssembler pagedResourcesAssembler;
public ContentRestController(
ContentService contentService,
PagedResourcesAssembler pagedResourcesAssembler) {
this.contentService = contentService;
this.pagedResourcesAssembler = pagedResourcesAssembler;
}
#RequestMapping(value = "/findContent", method = RequestMethod.GET)
#ResponseBody
public PagedResources<PersistentEntityResource> findContent(PersistentEntityResourceAssembler resourceAssembler) {
Page<Content> page = contentService.getContent();
#SuppressWarnings("unchecked")
PagedResources<PersistentEntityResource> pagedResources = pagedResourcesAssembler.toResource(page, resourceAssembler);
return pagedResources;
}
}
A call to /findContent takes 15s to fully respond (while data start streaming immideately after it is made, so this is like 15s serialization time).
After profiling I found out that the cause of the problem are persistent collection properties on Content. During serialization of a Content a new transaction is opened for every access attempt to the contentMappings collection, even when contentMappings was properly fetched before serialization inside contentService.getContent() call.
Opening an explicit transaction on a controller method did not help (cause it is closed after the method exits and before serialization occurs), but I was able to work around this behaviour using HttpServletResponse and manually serializing the response:
#RepositoryRestController
public class MyController {
private final ContentService contentService;
private final PagedResourcesAssembler pagedResourcesAssembler;
private final List<HttpMessageConverter> messageConverters;
public ContentRestController(
ContentService contentService,
PagedResourcesAssembler pagedResourcesAssembler,
List<HttpMessageConverter> messageConverters) {
this.contentService = contentService;
this.pagedResourcesAssembler = pagedResourcesAssembler;
this.messageConverters = messageConverters;
}
#RequestMapping(value = "/findContent", method = RequestMethod.GET)
#ResponseBody
#Transactional(readOnly = true)
public void findContent(PersistentEntityResourceAssembler resourceAssembler, HttpServletResponse response) throws IOException {
Page<Content> page = contentService.getContent();
#SuppressWarnings("unchecked")
PagedResources<PersistentEntityResource> pagedResources = pagedResourcesAssembler.toResource(page, resourceAssembler);
// manual response serialization
MediaType mediaType = MediaType.valueOf("application/hal+json");
ResponseEntity<String> responseEntity = messageConverters.stream()
.filter(messageConverter -> messageConverter.canWrite(pagedResources.getClass(), mediaType))
.findFirst()
.map(messageConverter -> {
HttpOutputMessage outputMessage = new HttpOutputMessage() {
private final OutputStream outputStream = new ByteArrayOutputStream();
private final HttpHeaders httpHeaders = new HttpHeaders();
#Override
public OutputStream getBody() throws IOException {
return outputStream;
}
#Override
public HttpHeaders getHeaders() {
return httpHeaders;
}
};
try {
messageConverter.write(pagedResources, mediaType, outputMessage);
return ResponseEntity.ok()
.headers(outputMessage.getHeaders())
.body(new String(((ByteArrayOutputStream) outputMessage.getBody()).toByteArray(), StandardCharsets.UTF_8));
} catch (IOException e) {
throw new IllegalStateException("Failed to convert output to " + mediaType.toString());
}
})
.orElseThrow(() -> new IllegalStateException("Failed to convert output to " + mediaType.toString()));
response.setContentType(mediaType.toString());
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.getWriter().write(responseEntity.getBody());
responseEntity.getHeaders().entrySet().stream()
.flatMap(entry -> entry.getValue().stream()
.map(value -> Tuples.of(entry.getKey(), value)))
.forEach(t -> response.addHeader(t.getT1(), t.getT2()));
response.flushBuffer();
}
}
This way response is received in 0.5s instead of 15s.
Problems I see with this workaround are e.g. completely ignoring RequestBodyAdvice / ResponseBodyAdvice processing, and the need to manually work with HttpServletResponse and HttpMessageConverter, effectively duplicating Spring code.
So my question is what I'm doing wrong, because if everything is right, I will open a bugreport on Spring Jira.
I've an heartbeat API implemeted using Spring REST service:
#RequestMapping(value = "heartbeat", method = RequestMethod.GET, consumes="application/json")
public ResponseEntity<String> getHeartBeat() throws Exception {
String curr_time = myService.getCurrentTime();
return Util.getResponse(curr_time, HttpStatus.OK);
}
And MyService.java has below method:
public String getCurrentTime() throws Exception {
String currentDateTime = null;
MyJson json = new MyJson();
ObjectMapper mapper = new ObjectMapper().configure(SerializationConfig.Feature.DEFAULT_VIEW_INCLUSION, false);
try {
Date currDate = new Date(System.currentTimeMillis());
currentDateTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").format(currDate);
json.setTime(currentDateTime);
ObjectWriter writer = mapper.writerWithView(Views.HeartBeatApi.class);
return writer.writeValueAsString(json);
} catch (Exception e) {
throw new Exception("Excpetion", HttpStatus.BAD_REQUEST);
}
}
It works as expected but have 2 issues:
When I invoke this API, Content-Type header is mandatory & I want to know how to make this header optional.
How to add "Accept" header so that it can support other format such as Google Protobuf?
Thanks!
If you don't want to require Content-Type exist and be "application/json", you can just omit the consumes section entirely.
"Accept" is available via the "produces" value, as opposed to "consumes." So if you wanted to support Google Protobuf OR application/json, you could do this:
#Controller
#RequestMapping(value = "/pets/{petId}", method = RequestMethod.GET, produces="application/json")
#ResponseBody
public ResponseEntity<String> getHeartBeat() throws Exception {
String curr_time = myService.getCurrentTime();
return Util.getResponse(curr_time, HttpStatus.OK);
}