Parse multiple JSON entries using org.springframework.boot.json.JsonParser - spring-boot

I'd like to parse a JSON Array using the native JsonParser available in Spring Boot:
String url="https://restservice.com";
RestTemplate restTemplate = new RestTemplate();
String resp = restTemplate.getForObject(url, String.class);
JsonParser springParser = JsonParserFactory.getJsonParser();
Map<String, Object> map = springParser.parseMap(resp);
That works if there's just one item in the Array. If multiple items are returned, an exception is thrown:
Caused by: org.springframework.boot.json.JsonParseException: Cannot parse JSON
at org.springframework.boot.json.AbstractJsonParser.tryParse(AbstractJsonParser.java:60) ~[spring-boot-2.1.0.RELEASE.jar!/:2.1.0.RELEASE]
at org.springframework.boot.json.JacksonJsonParser.parseMap(JacksonJsonParser.java:55) ~[spring-boot-2.1.0.RELEASE.jar!/:2.1.0.RELEASE]
at com.example.samplewebapp.DemoApplication.lambda$demo$2(DemoApplication.java:50) [classes!/:0.0.1-SNAPSHOT]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:813) [spring-boot-2.1.0.RELEASE.jar!/:2.1.0.RELEASE]
... 13 common frames omitted
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.LinkedHashMap` out of START_ARRAY token
What is the correct API to return a List of Map JSON objects?

Showing the JSON you are trying to parse would be helpful for getting a good answer to your problem.
Anyway, if you are trying to get a list you should use the parseList() method instead. This required doing a cast from objects to maps, but worked fine for me otherwise. Here's a quick example:
String json = "[{\"key\":\"value1\"}, {\"key\":\"value2\"}]";
JsonParser springParser = JsonParserFactory.getJsonParser();
List<Object> list = springParser.parseList(json);
for(Object o : list) {
if(o instanceof Map) {
Map<String,Object> map = (Map<String,Object>) o;
//do processing here
}
}
However, I would suggest calling the desired parser (Jackson, gson, etc.) directly if you need more control over the process. Jackson provides a handy ObjectMapper class to help with this sort of thing and avoid messy type conversions.

Related

How to change WebClient behaviour concerning LocalDateTime?

I am using Spring Boot 2.4.5 and try to request a REST API of another application using WebClient. I know that the other application provides the requested information as a Collection.
When I use an Object[] to receive the response:
Object[] ob = (Object[]) webClient
.get()
.uri(endpoint)
// .bodyValue(criteria)
.exchangeToMono(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
return response.bodyToMono(Object[].class);
} else if (response.statusCode()
.is4xxClientError()) {
return Mono.just("Error response");
} else {
return response.createException()
.flatMap(Mono::error);
}
}).block();
I can see that I receive a LinkedHashMap with all the values including a field:
date_of_declaration -> 2020-03-02T08:43:10
However, if possible, I want to let WebClient immediately convert the response into the designated DTOs...
DeclarationDTO[] ob = (DeclarationDTO[]) webClient
.get()
.uri(endpoint)
// .bodyValue(criteria)
.exchangeToMono(response -> {
if (response.statusCode()
.equals(HttpStatus.OK)) {
return response.bodyToMono(DeclarationDTO[].class);
} else if (response.statusCode()
.is4xxClientError()) {
return Mono.just("Error response");
} else {
return response.createException()
.flatMap(Mono::error);
}
}).block();
...I get an exception when a LocalDateTime object shall be deserialized.
org.springframework.core.codec.DecodingException: JSON decoding error: Cannot deserialize value of type `java.time.LocalDateTime` from String "02-03-2020 01:20:00": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '02-03-2020 01:20:00' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {HourOfAmPm=1, NanoOfSecond=0, SecondOfMinute=0, MicroOfSecond=0, MinuteOfHour=20, MilliOfSecond=0},ISO resolved to 2020-03-02 of type java.time.format.Parsed; nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.time.LocalDateTime` from String "02-03-2020 01:20:00": Failed to deserialize java.time.LocalDateTime: (java.time.format.DateTimeParseException) Text '02-03-2020 01:20:00' could not be parsed: Unable to obtain LocalDateTime from TemporalAccessor: {HourOfAmPm=1, NanoOfSecond=0, SecondOfMinute=0, MicroOfSecond=0, MinuteOfHour=20, MilliOfSecond=0},ISO resolved to 2020-03-02 of type java.time.format.Parsed
at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 1438] (through reference chain: java.lang.Object[][0]->de.xxx.myportal.api.infrastructure.dto.MyDTO["a_person"]->de.xxx.myportal.api.infrastructure.dto.APersonDTO["foobar"])
I think WebClient has an internal ObjectMapper, so maybe it is possible to modify this ObjectMapper during WebClient instantiation? ...or is there a better way to tell Webclient how to handle LocalDateTime? ...maybe a WebClient customized by a configuration or...?
I can't explain but after removing any JsonFormat annotation it works out of the box. (Weird!)
This may help https://www.baeldung.com/spring-boot-formatting-json-dates
In your DeclarationDTO you can use something like:
#JsonFormat(pattern="yyyy-MM-dd'T'HH:mm:ss")
#JsonProperty("date_of_declaration")
private LocalDateTime dateOfDeclaration;

Compilation error creating HttpHeaders using Collectors.toMap on the HttpServletRequest's headerNames Enumeration

I'm trying to copy all existing headers from an HttpServletRequest to a Spring HttpHeaders object to use with RestTemplate. This can be done easily in a loop on enumeration, but I'm getting this error while using streams:
HttpHeaders headers = new HttpHeaders();
Enumeration<String> existingHeaders = request.getHeaderNames();
headers.putAll(
Collections.list(existingHeaders)
.stream()
.collect(
Collectors.toMap(Function.identity(),HttpServletRequest::getHeader))
);
I declared the variable Enumeration<String> for the stream to not consider elements as Object but I'm still getting this compilation error at collect():
The method toMap(Function<? super T,? extends K>, Function<? super T,? extends U>)
in the type Collectors is not applicable for the arguments
(Function<Object,Object>, HttpServletRequest::getHeader)
The reason your code does not compile is that its Collectors.toMap(...) call is producing a Map<String, String>, but headers.putAll(...) requires a Map<String, List<String>>. This can be fixed by changing the Collectors.toMap(...) call to produce a compatible map:
HttpHeaders headers = new HttpHeaders();
Enumeration<String> existingHeaders = request.getHeaderNames();
headers.putAll(Collections.list(existingHeaders)
.stream()
.collect(Collectors.toMap(Function.identity(),
name -> Collections.list(request.getHeaders(name)))));
Since the same HTTP header can have multiple values, HttpHeaders implements Map<String,List<String>> rather than Map<String,String>. Therefore, putAll(map) requires a map with List<String> values, not one with String values.
To get the HttpHeaders from an HttpServletRequest you can use the ServletServerHttpRequest class in spring-web. Since the HttpHeaders class is also in spring-web, you should already have this class available on your classpath.
private static HttpHeaders getHttpHeaders(HttpServletRequest request) {
return new ServletServerHttpRequest(request).getHeaders();
}
This answer does not address why you're getting error you're receiving, though it does address the underlying problem you're attempting to solve: getting an HttpHeaders from an existing HttpServletRequest.

Using Jaxb Marshalling how can I override the XML Element names at runtime

In my code I have a class which is populated from a REST CALL like so:
RestTemplate restTemplate = new RestTemplate();
String JSONStr = restTemplate.getForObject(urlStr, String.class);
Gson gson = new Gson();
Products items = gson.fromJson(JSONStr, Products.class);
Then later I write the the list as XML
JAXBContext jaxbContext = JAXBContext.newInstance(ProductsList.class);
Marshaller productMarshaller2 = jaxbContext.createMarshaller();
productMarshaller2.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
OutputStream outputStream = new FileOutputStream("xml\\products.xml");
for (int index = 0; index < items.size(); index++){
System.out.println("Index = " + index);
ProductsList productList = items.get(index);
productMarshaller2.marshal(productList, outputStream);
}
outputStream.close();
This all works great and provides the expected output. The problem I have is that the JSON that I get has field names like "atr_203948" and I need to place them with names like "color" - but can not know the mapping between the names ahead of time. At runtime I have to execute a DB lookup to get the current map. The DB look up is easy and I have code that creates a HashMap with the oldName,newName pairs but I do not know inject that knowledge into the output of the Marshaller. Any help would be greatly appreciated.

accessing Apache camel header in spring expression language

I want to access camel exchange header value while parsing spring expression.
1.
Is it possible ? I am looking some thing like below.
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("request.headers['myHeader'].concat('!')");
String message = (String) exp.getValue();
System.out.println(message);
or can we use #Header and access the same ?
Is there any other way than adding the value to StandardEvaluationContext as variable and then parsing the same ?
Appreciate some quick help on the same.
2.
One more question is is there a way i can register all my custom functions at only one place ? I mean i want to avoid registering functions for every call to evaluate the expression. Currently i am doing it below way.
public static String evalExpr(String expr,
Map<String, Object> variables) throws NoSuchMethodException,
SecurityException {
StandardEvaluationContext context = new StandardEvaluationContext();
context.registerFunction("concat", CusExprs.class
.getDeclaredMethod("concat", String[].class));
context.registerFunction("substr", CustExprs.class
.getDeclaredMethod("substr", new Class[] { String.class,
Integer.class, Integer.class }));
context.setVariables(variables);
return parser.parseExpression(expr).getValue(context, String.class);
}

Spring RestRemplate postforobject with request parameter having integer value

I have a method in Spring rest service.
#RequestMapping(value = "test/process", method = RequestMethod.POST)
public #ResponseBody MyResponse processRequest(String RequestId, int count)
I am using Spring RestTemplate to call this service like this.
RestTemplate restTemplate = this.getRestTemplate();
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("RequestId", RequestId);
map.add("count", count);
restTemplate.postForObject(url, map,MyResponse.class);
When I try to invoke the client method I get the exception that no suitable HttpMessageConverter found for request type [java.lang.Integer]
org.springframework.http.converter.HttpMessageNotWritableException: Could not write request: no suitable HttpMessageConverter found for request type [java.lang.Integer]
at org.springframework.http.converter.FormHttpMessageConverter.writePart(FormHttpMessageConverter.java:310)
at org.springframework.http.converter.FormHttpMessageConverter.writeParts(FormHttpMessageConverter.java:270)
at org.springframework.http.converter.FormHttpMessageConverter.writeMultipart(FormHttpMessageConverter.java:260)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:200)
at org.springframework.http.converter.FormHttpMessageConverter.write(FormHttpMessageConverter.java:1)
at org.springframework.web.client.RestTemplate$HttpEntityRequestCallback.doWithRequest(RestTemplate.java:596)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:444)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:409)
at org.springframework.web.client.RestTemplate.postForObject(RestTemplate.java:287)
I know one of the ways is to pass all the parameters as String. But I might need to pass complex data types as parameters later.
What is the ways to achieve this.
I have googled and some option seem to be writing my own converters. How should I start about solving this problem.
The root cause of this error is that by specifying an Integer in the LinkedMultiValueMap, the RestTemplate will take that to mean that your request is a multipart request. There is no HttpMessageConverter registered by default that can handle writing values of type Integer to a request body.
As you said, you can handle this situation by changing the count to be a String. After all, there is no Integer type in HTTP request parameters. However, you were worried
But I might need to pass complex data types as parameters later.
Assume something like this
public #ResponseBody MyResponse processRequest(String RequestId, int count, Complex complex) {
with
public class Complex {
private String someValue;
private int intValue;
public String getSomeValue() {
return someValue;
}
public void setSomeValue(String someValue) {
this.someValue = someValue;
}
public int getIntValue() {
return intValue;
}
public void setIntValue(int intValue) {
this.intValue = intValue;
}
public String toString() {
return someValue + " " + intValue;
}
}
The the following will work just fine
MultiValueMap<String, Object> map = new LinkedMultiValueMap<String, Object>();
map.add("RequestId", "asd");
map.add("count", "42");
map.add("someValue", "complex");
map.add("intValue", "69");
restTemplate.postForObject(url, map,MyResponse.class);
Remember that the request parameters are used to populate the fields of model attributes by their names.
An even better solution would have you using a serialization standard like JSON or XML.

Resources