REST GET Response with Object containing a HashMap - java

I have following Rest service which queries a database, constructs multiple "Chat"-objects and returns them as an array:
#GET
#Path("/getChats")
#Produces(MediaType.APPLICATION_JSON)
public Chat[] getChats(#QueryParam("userId") String userId){
ArrayList<Chat> chats = getChatsDB(userId);
Chat[] chatAr = new Chat[chats.size()];
return chats.toArray(chatAr);
}
The "Chat"-class is a POJO:
import java.util.HashMap;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
#JsonIgnoreProperties(ignoreUnknown = true)
public class Chat {
private String userId1;
private String userId2;
private HashMap<String, String> msgs;
public Chat() {
msgs = new HashMap<>();
}
public String getUserId1() {
return userId1;
}
public void setUserId1(String userId1) {
this.userId1 = userId1;
}
public String getUserId2() {
return userId2;
}
public void setUserId2(String userId2) {
this.userId2 = userId2;
}
public void addMsg(String date, String msg){
msgs.put(date, msg);
}
public HashMap<String, String> getMsgs() {
return msgs;
}
}
The client code for getting this chat objects is :
public static Chat[] getChats() {
Chat[] chats = null;
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
String chatUrl = url+"getChats?userId="+user.getId();
chats = restTemplate.getForObject(chatUrl, Chat[].class);
for(Chat c: chats){
System.out.println(c.getUserId1());
System.out.println(c.getUserId2());
for(Map.Entry<String,String> e : c.getMsgs().entrySet()){
System.out.println(e.getKey() + e.getValue());
}
}
return chats;
The client recieves the chat objects, but without the HashMap with the messages. c.getUserId1 and c.getUserId2 return the correct values, but the HashMap is empty. This problem is only on the client side. The chat-objects in the servicemethod getChats(#QueryParam("userId") String userId) have the correct values in the HashMap.
Opening the link in the browser shows this:
[{"userId1":"414","userId2":"12"}]

You need to have both getter and setter in your POJO for the inner map.
public class Chat {
private HashMap<String, String> msgs;
public void setMsgs(HashMap<String, String> msgs) {
this.msgs = msgs;
}
// rest of the code ...
}
If you don't want to change pojo implementation for some reason, you can setup Jackson to use private fields and not getters/setters, something like this: how to specify jackson to only use fields - preferably globally
For some reason your serverside sends you
"maps":{"entry":[{"key":"key1","value":"value1"}]}
instead of
"maps":{"key1":"value1","key2":"value2"}
You can probably solve it with just client side pojo changes like this:
public void setMsgs(Map<String, List<Map<String,String>>> entries){
for (Map<String, String> entry: entries.get("entry"))
msgs.put(entry.get("key"),entry.get("value"));
}

Related

Swagger configuration in spring boot project, for same request model, need to display different responses, example in request model

Below is the definition of my 2 APIs:
#PutMapping("/valA")
public ResponseEntity<DummyResponse> getValA(#RequestBody DummyModel model) {
DummyResponse dummyResponse = new DummyResponse();
dummyResponse.setResA(model.getValA());
return new ResponseEntity<>(dummyResponse, HttpStatus.OK);
}
#PutMapping("/valB")
public ResponseEntity<DummyResponse> getValB(#RequestBody DummyModel model) {
DummyResponse dummyResponse = new DummyResponse();
dummyResponse.setResB(model.getValB());
return new ResponseEntity<>(dummyResponse, HttpStatus.OK);
}
DummyModel.java
package com.dummy.mo.model;
import java.io.Serializable;
import lombok.Data;
#Data
public class DummyModel implements Serializable {
private String valA;
private String valB;
}
Now in swagger, For both the apis, example shows like below:
But, my reuirement is to show only valA in first api and valB in second api in swagger example. I mean, I only want to show the params which are required to the corresponding APIs.
Is there any annotation or configuration out there to define required request parameters at API/Controller level.
Note that: I cannot change the API structure or model class.
Why don't you split the DummyModel class.
What if we, create 2 interfaces like below :
public interface One {
String valB = null;
public default String getBalB(){
return valB;
}
}
public interface Two {
String valA = null;
public default String getBalA(){
return valA;
}
}
Dummy Model class
#Data
public class DummyModel implements One, Two{
private String valB = null;
private String valA = null;
public DummyModel(String valA,String valB) {
this.valB = valB;
this.valA = valA;
}
}
Update your apis :
#RestController
public class TestClass {
#PutMapping("/valA")
public ResponseEntity<One> getValA(#RequestBody One model) {
DummyModel dummyResponse = new DummyModel(null,model.getBalB());
return new ResponseEntity<>(dummyResponse, HttpStatus.OK);
}
#PutMapping("/valB")
public ResponseEntity<Two> getValB(#RequestBody Two model) {
DummyModel dummyResponse = new DummyModel(model.getBalA(),null);
return new ResponseEntity<>(dummyResponse, HttpStatus.OK);
}
}

How to bind text response to json and put in object on a declarative client on micronaut?

I made my declarative http client on my app built in micronaut. This need to consume a services which responds with text/html type.
I manage to get a list but with LinkedHashMap inside. And I want them to be objects of Pharmacy
My question is: how I can transform that response into a List of object?
#Client("${services.url}")
public interface PharmacyClient {
#Get("${services.path}?${services.param}=${services.value}")
Flowable<List<Pharmacy>> retrieve();
}
public class StoreService {
private final PharmacyClient pharmacyClient;
public StoreService(PharmacyClient pharmacyClient) {
this.pharmacyClient = pharmacyClient;
}
public Flowable<List<Store>> all() {
Flowable<List<Pharmacy>> listFlowable = this.pharmacyClient.retrieve();
return listFlowable
.doOnError(throwable -> log.error(throwable.getLocalizedMessage()))
.flatMap(pharmacies ->
Flowable.just(pharmacies.stream() // here is a list of LinkedHashMap and i'd like to user Pharmacy objects
.map(pharmacy -> Store.builder().borough(pharmacy.getBoroughFk()).build())
.collect(Collectors.toList())
)
);
}
}
Code: https://github.com/j1cs/drugstore-demo/tree/master/backend
There is no fully-fledged framework AFAIK that provides support for HTML content to POJO mapping (which is usually referred to as scraping) as is the case for Micronaut, .
Meanwhile you can easily plug a converter bean based on jspoon intercepting and transforming your API results in equivalent POJOs:
class Root {
#Selector(value = ".pharmacy") List<Pharmacy> pharmacies;
}
class Pharmacy {
#Selector(value = "span:nth-child(1)") String name;
}
#Client("${services.minsal.url}")
public interface PharmacyClient {
#Get("${services.minsal.path}?${services.minsal.param}=${services.minsal.value}")
Flowable<String> retrieve();
}
#Singleton
public class ConverterService {
public List<Pharmacy> toPharmacies(String htmlContent) {
Jspoon jspoon = Jspoon.create();
HtmlAdapter<Root> htmlAdapter = jspoon.adapter(Root.class);
return htmlAdapter.fromHtml(htmlContent).pharmacies;
}
}
public class StoreService {
private final PharmacyClient pharmacyClient;
private final ConverterService converterService;
public StoreService(PharmacyClient pharmacyClient, ConverterService converterService) {
this.pharmacyClient = pharmacyClient;
this.converterService = converterService;
}
public Flowable<List<Store>> all() {
Flowable<List<Pharmacy>> listFlowable = this.pharmacyClient.retrieve().map(this.converterService::toPharmacies)
return listFlowable
.doOnError(throwable -> log.error(throwable.getLocalizedMessage()))
.flatMap(pharmacies ->
Flowable.just(pharmacies.stream() // here is a list of LinkedHashMap and i'd like to user Pharmacy objects
.map(pharmacy -> Store.builder().borough(pharmacy.getBoroughFk()).build())
.collect(Collectors.toList())
)
);
}
}
I ended up with this.
#Client("${services.url}")
public interface PharmacyClient {
#Get(value = "${services.path}?${services.param}=${services.value}")
Flowable<Pharmacy[]> retrieve();
}
public class StoreService {
private final PharmacyClient pharmacyClient;
public StoreService(PharmacyClient pharmacyClient) {
this.pharmacyClient = pharmacyClient;
}
public Flowable<List<Store>> all() {
Flowable<Pharmacy[]> flowable = this.pharmacyClient.retrieve();
return flowable
.switchMap(pharmacies ->
Flowable.just(Arrays.stream(pharmacies)
.map(pharmacyStoreMapping)
.collect(Collectors.toList())
)
).doOnError(throwable -> log.error(throwable.getLocalizedMessage()));
}
}
Still I want to know if i can change arrays to list in the declarative client.
Meanwhile i think this it's a good option.
UPDATE
I have been wrong all this time. First of all I don't need to add a list to the flowable because when the framework exposes the service it responds with a list of elements already.
So finally I did this:
#Client("${services.url}")
public interface PharmacyClient {
#Get(value = "${services.path}?${services.param}=${services.value}")
Flowable<Pharmacy> retrieve();
}
public class StoreService {
private final PharmacyClient pharmacyClient;
public StoreService(PharmacyClient pharmacyClient) {
this.pharmacyClient = pharmacyClient;
}
public Flowable<Store> all() {
Flowable<Pharmacy> flowable = this.pharmacyClient.retrieve();
return flowable
.switchMap(pharmacyPublisherFunction)
.doOnError(throwable -> log.error(throwable.getLocalizedMessage()));
}
As we can see the http client automatically transform the text/html data into json and it parses it well. I don't know why really. Maybe #JeffScottBrown can gives us some hints.

How to pass hashmap as payload in RES and how to test it through POSTMAN

I have a rest API with the following URL
#PostMapping(path = "/Employees/employees")
private ResponseEntity<Map<String, BigDecimal>> availabilityCalculator(#RequestBody ReqOb req, Map<String, BigDecimal> testMap) {}
what annotation should I use for the map(like RequestBody for Object). Can I use RequestBody itself considering the map is also a type of object? 2. How should I pass - a hashmap and an object as payload for testing it through POSTMAN
Create a class that will contain that map and pass it like that. For example:
public class CalculationStatsDto {
private Map<String, BigDecimal> testMap;
public CalculationStatsDto () {
}
public Map<String, BigDecimal> getTestMap() {
return testMap;
}
public void setTestMap(Map<String, BigDecimal> testMap) {
this.testMap = testMap;
}
}
And your rest method should be:
#PostMapping(path = "/Employees/employees")
private ResponseEntity<CalculationStatsDto> availabilityCalculator(#RequestBody
CalculationStatsDto calculationStatsDto) {}
And if you need also that 'ReqOb req' in the request body then you can put it in your entity class:
public class CalculationStatsDto {
private Map<String, BigDecimal> testMap;
private ReqOb req;
public CalculationStatsDto() {
}
public Map<String, BigDecimal> getTestMap() {
return testMap;
}
public void setTestMap(Map<String, BigDecimal> testMap) {
this.testMap = testMap;
}
public ReqOb getReq() {
return req;
}
public void setReq(ReqOb req) {
this.req = req;
}
}
With the last one you will wrap both of it in one request body.

Amazon Lambda timing out when attempting to initialize a client for DynamoDB

I have the following Java class that is uploaded on Amazon's Lambda service:
public class DevicePutHandler implements RequestHandler<DeviceRequest, Device> {
private static final Logger log = Logger.getLogger(DevicePutHandler.class);
public Device handleRequest(DeviceRequest request, Context context) {
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();
DynamoDBMapper mapper = new DynamoDBMapper(client);
if (request == null) {
log.info("The request had a value of null.");
return null;
}
log.info("Retrieving device");
Device deviceRetrieved = mapper.load(Device.class, request.getDeviceId());
log.info("Updating device properties");
deviceRetrieved.setBuilding(request.getBuilding());
deviceRetrieved.setMotionPresent(request.getMotionPresent());
mapper.save(deviceRetrieved);
log.info("Updated device has been saved");
return deviceRetrieved;
}
}
I also have an Execution Role set that gives me complete control over DynamoDB. My permissions should be perfectly fine since I've used the exact same permissions with other projects that used Lambda and DynamoDB in this exact manner (the only difference being a different request type).
The intended point of this class is to have it be called by API Gateway (API Gateway -> Lambda -> DynamoDB), but for now I simply am trying to test it on Lambda (Lambda -> DynamoDB).
For reference, in case it matters, here is the DeviceRequest class:
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonPropertyOrder({ "deviceId", "building", "motionPresent" })
public class DeviceRequest {
#JsonProperty("deviceId")
private String deviceId;
#JsonProperty("building")
private String building;
#JsonProperty("motionPresent")
private Boolean motionPresent;
#JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
#JsonProperty("deviceId")
public String getDeviceId() {
return deviceId;
}
#JsonProperty("deviceId")
public void setDeviceId(String deviceId) {
this.deviceId = deviceId;
}
#JsonProperty("building")
public String getBuilding() {
return building;
}
#JsonProperty("building")
public void setBuilding(String building) {
this.building = building;
}
#JsonProperty("motionPresent")
public Boolean getMotionPresent() {
return motionPresent;
}
#JsonProperty("motionPresent")
public void setMotionPresent(Boolean motionPresent) {
this.motionPresent = motionPresent;
}
#JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
#JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
And here is the Device class:
#DynamoDBTable(tableName="DeviceTable")
public class Device {
private String deviceID;
private String building;
private String queue;
private boolean motionPresent;
#DynamoDBHashKey(attributeName="Device ID")
public String getDeviceID() {
return deviceID;
}
public void setDeviceID(String deviceID) {
this.deviceID = deviceID;
}
#DynamoDBAttribute(attributeName="Motion Present")
public boolean getMotionPresent() {
return motionPresent;
}
public void setMotionPresent(boolean motionPresent) {
this.motionPresent = motionPresent;
}
#DynamoDBAttribute(attributeName="Building")
public String getBuilding() {
return this.building;
}
public void setBuilding(String building) {
this.building = building;
}
#DynamoDBAttribute(attributeName="Queue")
public String getQueue() {
return this.queue;
}
public void setQueue(String queue) {
this.queue = queue;
}
}
Here is the JSON input that I'm trying to test the Lambda with:
{
"deviceId": "test_device_name",
"building": "building1",
"motionPresent": false
}
No exceptions whatsoever are thrown (I've tried wrapping it around a try/catch block) and the lambda timing out is the only thing that happens. I've tried using log/print statements at the very beginning prior to the initialization of the DynamoDB client to see if the request can be read properly and it does appear to properly parse the JSON fields. I've also separated the client builder out and found that the builder object is able to be initialized, but the timing out comes from when the builder calls build() to make the client.
If anyone has any insight into why this timing out is occurring, please let me know!
Turns out that by bumping up the timout period AND the allotted memory, the problem get solved. Not sure why it works since the lambda always indicated that its memory usage was under the previously set limit, but oh well. Wish that in the future Amazon will provide better error feedback that indicates if a lambda needs more resources to run.

Mapping URL parameters with dashes to object in Spring Web MVC

Mapping URL request parameters with Spring MVC to an object is fairly straightforward if you're using camelCase parameters in your request, but when presented with hyphen delimited values, how do you map these to an object?
Example for reference:
Controller:
#RestController
public class MyController {
#RequestMapping(value = "/search", method = RequestMethod.GET)
public ResponseEntity<String> search(RequestParams requestParams) {
return new ResponseEntity<>("my-val-1: " + requestParams.getMyVal1() + " my-val-2: " + requestParams.getMyVal2(), HttpStatus.OK);
}
}
Object to hold parameters:
public class RequestParams {
private String myVal1;
private String myVal2;
public RequestParams() {}
public String getMyVal1() {
return myVal1;
}
public void setMyVal1(String myVal1) {
this.myVal1 = myVal1;
}
public String getMyVal2() {
return myVal2;
}
public void setMyVal2(String myVal2) {
this.myVal2 = myVal2;
}
}
A request made like this works fine:
GET http://localhost:8080/search?myVal1=foo&myVal2=bar
But, what I want is for a request with hyphens to map to the object, like so:
GET http://localhost:8080/search?my-val-1=foo&my-val-2=bar
What do I need to configure in Spring to map url request parameters with hyphens to fields in an object? Bear in mind that we may have many parameters, so using a #RequestParam annotation for each field is not ideal.
I extended ServletRequestDataBinder and ServletModelAttributeMethodProcessor to solve the problem.
Consider that your domain object may already be annotated with #JsonProperty or #XmlElement for serialization. This example assumes this is the case. But you could also create your own custom annotation for this purpose e.g. #MyParamMapping.
An example of your annotated domain class is:
public class RequestParams {
#XmlElement(name = "my-val-1" )
#JsonProperty(value = "my-val-1")
private String myVal1;
#XmlElement(name = "my-val-2")
#JsonProperty(value = "my-val-2")
private String myVal2;
public RequestParams() {
}
public String getMyVal1() {
return myVal1;
}
public void setMyVal1(String myVal1) {
this.myVal1 = myVal1;
}
public String getMyVal2() {
return myVal2;
}
public void setMyVal2(String myVal2) {
this.myVal2 = myVal2;
}
}
You will need a SerletModelAttributeMethodProcessor to analyze the target class, generate a mapping, invoke your ServletRequestDataBinder.
public class KebabCaseProcessor extends ServletModelAttributeMethodProcessor {
public KebabCaseProcessor(boolean annotationNotRequired) {
super(annotationNotRequired);
}
#Autowired
private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
private final Map<Class<?>, Map<String, String>> replaceMap = new ConcurrentHashMap<Class<?>, Map<String, String>>();
#Override
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest nativeWebRequest) {
Object target = binder.getTarget();
Class<?> targetClass = target.getClass();
if (!replaceMap.containsKey(targetClass)) {
Map<String, String> mapping = analyzeClass(targetClass);
replaceMap.put(targetClass, mapping);
}
Map<String, String> mapping = replaceMap.get(targetClass);
ServletRequestDataBinder kebabCaseDataBinder = new KebabCaseRequestDataBinder(target, binder.getObjectName(), mapping);
requestMappingHandlerAdapter.getWebBindingInitializer().initBinder(kebabCaseDataBinder, nativeWebRequest);
super.bindRequestParameters(kebabCaseDataBinder, nativeWebRequest);
}
private static Map<String, String> analyzeClass(Class<?> targetClass) {
Field[] fields = targetClass.getDeclaredFields();
Map<String, String> renameMap = new HashMap<String, String>();
for (Field field : fields) {
XmlElement xmlElementAnnotation = field.getAnnotation(XmlElement.class);
JsonProperty jsonPropertyAnnotation = field.getAnnotation(JsonProperty.class);
if (xmlElementAnnotation != null && !xmlElementAnnotation.name().isEmpty()) {
renameMap.put(xmlElementAnnotation.name(), field.getName());
} else if (jsonPropertyAnnotation != null && !jsonPropertyAnnotation.value().isEmpty()) {
renameMap.put(jsonPropertyAnnotation.value(), field.getName());
}
}
if (renameMap.isEmpty())
return Collections.emptyMap();
return renameMap;
}
}
This KebabCaseProcessor will use reflection to get a list of mappings for your request object. It will then invoke the KebabCaseDataBinder - passing in the mappings.
#Configuration
public class KebabCaseRequestDataBinder extends ExtendedServletRequestDataBinder {
private final Map<String, String> renameMapping;
public KebabCaseRequestDataBinder(Object target, String objectName, Map<String, String> mapping) {
super(target, objectName);
this.renameMapping = mapping;
}
protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
super.addBindValues(mpvs, request);
for (Map.Entry<String, String> entry : renameMapping.entrySet()) {
String from = entry.getKey();
String to = entry.getValue();
if (mpvs.contains(from)) {
mpvs.add(to, mpvs.getPropertyValue(from).getValue());
}
}
}
}
All that remains now is to add this behavior to your configuration. The following configuration overrides the default configuration that the #EnableWebMVC delivers and adds this behavior to your request processing.
#Configuration
public static class WebContextConfiguration extends WebMvcConfigurationSupport {
#Override
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(kebabCaseProcessor());
}
#Bean
protected KebabCaseProcessor kebabCaseProcessor() {
return new KebabCaseProcessor(true);
}
}
Credit should be given to #Jkee. This solution is derivative of an example he posted here: How to customize parameter names when binding spring mvc command objects.
One way I can think of getting around the hyphens is to use HttpServletRequestWrapper class to wrap the original request.
Parse all the request parameters in this class and convert all hyphenated parameters into camelcase. After this, spring will be able to automatically map those parameters to your POJO classes.
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> camelCasedParams = new Hashmap();
public CustomRequestWrapper(HttpServletRequest req){
//Get all params from request.
//Transform each param name from hyphenated to camel case
//Put them in camelCasedParams;
}
public String getParameter(String name){
return camelCasedParams.get(name);
}
//Similarly, override other methods related to request parameters
}
Inject this request wrapper from J2EE filter. You can refer to below link for a tutorial on injecting request wrappers using filter.
http://www.programcreek.com/java-api-examples/javax.servlet.http.HttpServletRequestWrapper
Update your web xml to include filter and its filter mapping.

Categories