Following is the body of POST request to my endpoint:
{
"availabilityMap":
{
"2021-07-18":["9AM-10AM", "OT"],
"2021-07-19":["9AM-10AM", "OPD"],
.
.
}
}
and following is the skeleton of my controller:
#PostMapping(value = "/appointment", consumes = MediaType.APPLICATION_JSON_VALUE,
produces= MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity newAppointment(#RequestBody /* WHAT TO PUT HERE?*/) {
}
How do I define an entity so that the #RequestBody is automatically converted to an entity? Is that possible?
In general, we prepare dto or resource for request and response. If your requests or responses are related to the database, it should be dto, otherwise you should mark it as a resource and you can perform automatic mapping operations using MapStruct.
#Data
public class RequestDto {
public Map<String, List<String>> availabilityMap;
}
public class RequestVO {
public Map<String, List<String>> availabilityMap;
public Map<String, List<String>> getAvailabilityMap() {
return availabilityMap;
}
public void setAvailabilityMap(Map<String, List<String>> availabilityMap) {
this.availabilityMap = availabilityMap;
}
}
{} can be represented as an object or a map, and [] can be represented as an implementation class of a Collection. (Commonly used are List and Set, which are also interfaces. The specific classes are specified by the sequence number framework. You can also use implementation classes such as HashMap.) For {}, using an object or a map depends on whether the field is fixed or not. In the example, availabilityMap is a fixed-name field, so the object is used. 2021-07-18 is an indefinite date or other class, so use Map. For one object, if its field is another object, you may need to write a new java class.
Related
I have one Java REST API which is used by 2 different consumers. By default REST principles my API should define the names of request headers. But now I have not common situation. Consumers use different security layer which provides different headers which means same parameter in both ways.
Example method: (not real code)
For 1st consumer:
#PostMapping("/number")
Integer getNumber(#RequestHeader("no") String number, #RequestBody User user) {
/*...*/
}
For 2nd consumer:
#PostMapping("/number")
Integer getNumber(#RequestHeader("number") String number, #RequestBody User user) {
/*...*/
}
I have up to 10 methods in one controller and they should be with same name and logic, but different header. The request path prefix could be different.
Question:
How to simplify REST controller and don't create 2 different controllers with same methods and same logic?
What I tried:
I tried several examples to create one controller with 2 different interfaces with same methods, but different mapping.
Example:
Controller class
#RestController
#RequestMapping(path ="/application")
#Api(tags = {"application"})
public class ApplicationController implements AppMapping1, AppMapping2 {
#Override
public Integer getNumber(String number, User user) {
/*...*/
}
}
First interface
interface AppMapping1 {
#PostMapping("/num")
Integer getNumber(#RequestHeader("num") String number, #RequestBody User user);
}
Second interface
interface AppMapping2 {
#PostMapping("/number")
Integer getNumber(#RequestHeader("number") String number, #RequestBody User user);
}
Result:
Controller maps only with the first interface. So http://.../application/num works fine, but http://.../application/number - gets 404 error code. That means Java Spring-Boot doesn't have such functionality. Need some more ideas.
Project developed with Java 8; spring-boot:2.1.1.RELEASE; gradle
According to this , If we're not sure which headers will be present, or we need more of
them than we want in our method's signature, we can use the
#RequestHeader annotation without a specific name.
You have a few choices for variable type: a Map, a MultiValueMap or an HttpHeaders object.
Sample
#PostMapping("/number")
public Integer getNumber(#RequestHeader Map<String, String> headers) {
if (Optional.ofNullable(headers.get("no")).isPresent()){
//...
}
else if (Optional.ofNullable(headers.get("number")).isPresent())
{
//...
}
}
It is not maintenance friendly to repeat the same block of code twice or more times just to receive the same input with different names (number and no). Instead, it is advisable to read all the headers and traverse through it to fetch input using different names.
Sample Code
#PostMapping("/number")
public Integer getNumber(#RequestHeader Map<String, String> headers) {
String number = headers.containsKey("number") ? headers.get("number") : headers.get("no");
if(Objects.isNull(number)) {
throw new RuntimeException("Number input not received from header!");
}
// relevant processing
}
I found this answer on https://www.logicbig.com/tutorials/spring-framework/spring-web-mvc/spring-mvc-request-header.html
Avoid ambiguity by using #RequestMapping(headers = ....)
We can fix the ambiguity similar to #RequestParam where we used
'params' . In case of #RequestHeader we can define related headers in
#RequestMapping annotation.
#Controller
#RequestMapping("trades")
public class TradesController {
#RequestMapping(headers = "User-Agent")
public String handleAllTradesRequests (#RequestHeader("User-Agent") String userAgent,
Model model) {
model.addAttribute("msg", "all trades requests, User-Agent header : "
+ userAgent);
return "my-page";
}
#RequestMapping(headers = "From")
public String handleRequestByFromHeader (#RequestHeader("From") String from,
Model model) {
model.addAttribute("msg", "trade request by From header : " + from);
return "my-page";
}
You could remove the #RequestHeader annotation and consider doing the following:
#PostMapping("/number")
Integer getNumber(HttpServletRequest request, #RequestBody User user) {
String number = request.getHeader("num");
if(number == null){
number = request.getHeader("number");
}
/*...*/
}
If you want a cleaner approach, consider creating a util class that takes the HttpServletRequest object and returns the desired header value.
The best way is to add the HttpServletRequest as an argument of your single controller and do some logic with the header map provided by the HttpServletRequest object.
If you want to see a full example take a look here. I have implemented I single controller that wraps all my logic accordingly to headers/methods and so on. You can customize the logic as you want with the HttpServletRequest.
I have a Spring 4.3.3 #RestController which manages an entity type via Lists.
#RestController
#RequestMapping("...")
public class EntityRestController {
#PostMapping
public void doSomeWork(#RequestBody final List<Entity> entities) { ... }
}
I discovered sometimes I may receive a request where the body consists not of an array, but a single JSON object.
I'm using Gson as the default serializer/deserializer and obviously it throws an exception.
JSON parse error: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT
What would the better way to tackle this problem be (at the Controller level)?
You could use HttpServletRequest in method parameter, like:
#RestController
#RequestMapping("...")
public class EntityRestController {
#PostMapping
public void doSomeWork(HttpServletRequest request) {
// Do your things here..
}
}
And this question has many answers how to parse request body within HttpServletRequest:
Use apache commons-IO : IOUtils.toString(request.getReader()).
Use request.getParameterMap() :
Map<String, String[]> map = request.getParameterMap();
for(String paramName : map.keySet()) {
String[] paramValues = map.get(paramName);
for(String valueOfParam : paramValues) {
// Do your things..
}
}
Or other techniques based on your need and preferences.
HTH
I have the following method in my Spring MVC #Controller :
#RequestMapping(method = RequestMethod.GET)
public String testUrl(#RequestParam(value="test") Map<String, String> test) {
(...)
}
I call it like this :
http://myUrl?test[A]=ABC&test[B]=DEF
However the "test" RequestParam variable is always null
What do I have to do in order to populate "test" variable ?
As detailed here
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestParam.html
If the method parameter is Map or MultiValueMap and a parameter name is not specified, then the map parameter is populated with all request parameter names and values.
So you would change your definition like this.
#RequestMapping(method = RequestMethod.GET)
public String testUrl(#RequestParam Map<String, String> parameters)
{
(...)
}
And in your parameters if you called the url http://myUrl?A=ABC&B=DEF
You would have in your method
parameters.get("A");
parameters.get("B");
You can create a new class that contains the map that should be populated by Spring and then use that class as a parameter of your #RequestMapping annotated method.
In your example create a new class
public static class Form {
private Map<String, String> test;
// getters and setters
}
Then you can use Form as a parameter in your method.
#RequestMapping(method = RequestMethod.GET)
public String testUrl(Form form) {
// use values from form.getTest()
}
Spring doesn't have default conversion strategy from multiple parameters with the same name to HashMap. It can, however, convert them easily to List, array or Set.
#RequestMapping(value = "/testset", method = RequestMethod.GET)
public String testSet(#RequestParam(value = "test") Set<String> test) {
return "success";
}
I tested with postman like http://localhost:8080/mappings/testset?test=ABC&test=DEF
You will see set having data, [ABC, DEF]
Your question needs to be considered from different points of view.
first part:
as is mentioned in the title of the question, is how to have Map<String, String> as #RequestParam.
Consider this endpoint:
#GetMapping(value = "/map")
public ResponseEntity getData(#RequestParam Map<String, String> allParams) {
String str = Optional.ofNullable(allParams.get("first")).orElse(null);
return ResponseEntity.ok(str);
}
you can call that via:
http://<ip>:<port>/child/map?first=data1&second=data2
then when you debug your code, you will get these values:
> allParams (size = 2)
> first = data1
> second = data2
and the response of the requested url will be data1.
second part:
as your requested url shows (you have also said that in other answers' comments) ,you need an array to be passed by url.
consider this endpoint:
public ResponseEntity<?> getData (#RequestParam("test") Long[] testId,
#RequestParam("notTest") Long notTestId)
to call this API and pass proper values, you need to pass parameters in this way:
?test=1&test=2¬Test=3
all test values are reachable via test[0] or test[1] in your code.
third part:
have another look on requested url parameters, like: test[B]
putting brackets (or [ ]) into url is not usually possible. you have to put equivalent ASCII code with % sign.
for example [ is equal to %5B and ] is equal to %5D.
as an example, test[0] would be test%5B0%5D.
more ASCII codes on: https://ascii.cl/
I faced a similar situation where the client sends two groups of variable parameters. Let's call these groups foo and bar. A request could look like:
GET /search?page=2&size=10&foo[x]=aaa&foo[y]=bbb&bar[z]=ccc
I wanted to map these parameters to two distinct maps. Something like:
#GetMapping("/search")
public Page<...> search(Pageable pageable,
#RequestParam(...) Map<String, String> foo,
#RequestParam(...) Map<String, String> bar) {
...
}
#RequestParam didn't work for me, too. Instead I created a Model class with two fields of type Map<> matching the query parameter names foo and bar (#Data is lombok.Data).
#Data
public static class FooBar {
Map<String, String> foo;
Map<String, String> bar;
}
My controller code has changed to:
#GetMapping("/search")
public Page<...> search(Pageable pageable, FooBar fooBar) {
...
}
When requesting GET /search?page=2&size=10&foo[x]=aaa&foo[y]=bbb&bar[z]=ccc Spring instantiated the Maps and filled fooBar.getFoo() with keys/values x/aaa and y/bbb and fooBar.getBar() with z/ccc.
you can use MultiValueMap
MultiValueMap<String, String>
#RequestMapping(method = RequestMethod.GET)
public String testUrl(#RequestParam(value="test") MultiValueMap<String, String> test) {
(...)
}
and while testing don't use test[A],test[B]. just use it as stated below.
http://myUrl?test=ABC&test=DEF
test result will be in below format when you print it.
test = {[ABC, DEF]}
I'm using Amazon's DynamoDBMapper Java class to save data to a DynamoDB table. This code needs to work for data structured in multiple different ways, so I would like to stay away from writing particularly structure-specific code. For this reason, I store the code as JSON objects in Java -- which are basically glorified HashMaps.
I would like to store these JSON objects into DynamoDB as Dynamo's relatively new JSON Document type.
The way the DynamoDBMapper API works is essentially that you write a Java class (typically a POJO), then add some annotations, then pass your objects of that class into DynamoDBMapper so that it can then put items into the database with the structure of the Java class. This works well for many aspects of what I'm doing, but not with the fact that I want these classes to contain arbitrarily-structured JSON documents. This is the way you're meant to store JSON documents using DynamoDBMapper, and as you can see, it doesn't allow for the structure of the documents to be arbitrary.
I realize I could use Dynamo's putItem() to pass the jsons as Strings into Item objects -- I just wanted to see if what I want to do is possible with DynamoDBMapper before I shift my approach.
You can try using the DynamoDB Java document SDK instead of the object mapper. This allows you to serialize and deserialize JSON strings using the fromJSON and toJSON methods in the Item class. Check out http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/JavaDocumentAPIItemCRUD.html.
Here's how I came up with my answer of how to store arbitrary Map objects in DynamoDB. This is extremely useful for archiving REST API responses that have been unmarshaled to foreign objects. I'm personally using this to archive REST responses from the PayPal Payment API. I don't care what variables they use in their REST API or the structure of their POJO / beans. I just want to make sure I save everything.
#DynamoDBTable(tableName = "PaymentResponse")
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
#JsonSubTypes({
#JsonSubTypes.Type(value = PayPalPaymentResponse.class, name = "PayPalPaymentResponse"),
#JsonSubTypes.Type(value = BatchPayPalPaymentResponse.class, name = "BatchPayPalPaymentResponse")}
)
public abstract class PaymentResponse {
// store any arbitrary REST resrponse data in map form so we don't have to worry about the
// structure or the actual response itself
protected Map<String, String> paymentResponseData = Maps.newHashMap();
public PaymentResponse(PaymentResponseType paymentResponseType) {
this.paymentResponseType = paymentResponseType;
}
public Map<String, String> getPaymentResponseData() { return paymentResponseData; }
public void setPaymentResponseData(Map<String, String> paymentResponseData) { this.paymentResponseData = paymentResponseData; }
#Override
public String toString() {
return Arrays.toString(paymentResponseData.entrySet().toArray());
}
}
public class ConverterUtils {
public static BatchPayPalPaymentResponse getBatchPayPalPaymentResponse(PayoutBatch payoutBatch) throws IOException {
//read in the PayoutBatch response data and convert it first to a JSON string and then convert the
//JSON string into a Map<String, String>
Map<String, String> responseData = objectMapper.readValue(objectMapper.writeValueAsString(payoutBatch), new TypeReference<Map<String, String>>() {});
BatchPayPalPaymentResponse batchPayPalPaymentResponse = new BatchPayPalPaymentResponse(responseData);
return batchPayPalPaymentResponse;
}
public static PayPalPaymentResponse getSinglePayPalPaymentResponse(PayoutItemDetails payoutItemDetails) throws IOException {
//read in the paypal PayoutItemDetails response data and convert it first to a JSON string and then convert the
//JSON string into a Map<String, String>
Map<String, String> responseData = objectMapper.readValue(objectMapper.writeValueAsString(payoutItemDetails), new TypeReference<Map<String, String>>() {});
PayPalPaymentResponse payPalPaymentResponse = new PayPalPaymentResponse(responseData);
return payPalPaymentResponse;
}
}
public class BatchPayPalPaymentResponse extends PaymentResponse {
public BatchPayPalPaymentResponse(Map<String, String> responseData) {
super(responseData);
}
....
....
....
}
public class PayPalPaymentResponse extends PaymentResponse {
public PayPalPaymentResponse(Map<String, String> responseData) {
super(responseData);
}
....
....
....
}
Now you can just call mapper.save(instanceOfPaymentResponse). Note that my code also includes how to use a Jackson parser to pick and choose which sub-class of PaymentResponse to unmarshal too. That's because I use a DynamoDBTypeConverter to marshal my class to a string before putting it into the database.
Finally, I'll throw in my converter for completeness so it all hopefully makes sense.
public class PaymentResponseConverter implements DynamoDBTypeConverter<String, PaymentResponse> {
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
}
#Override
public String convert(PaymentResponse object) {
try {
return objectMapper.writeValueAsString(object);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(String.format("Received invalid instance of PaymentResponse and cannot marshal it to a string (%s)", e.getMessage()));
}
}
#Override
public PaymentResponse unconvert(String object) {
try {
return objectMapper.readValue(object, PaymentResponse.class);
} catch (IOException e) {
throw new IllegalArgumentException(String.format("Unable to convert JSON to instance of PaymentResponse. This is a fatal error. (%s)", e.getMessage()));
}
}
}
I had the same problem and went the route of serializing and deserializing objects to json string by myself and then just store them as strings. The whole Document concept of DynamoDB is IMHO just a glorified object serializer. Only if you need to access attributes inside your object in dynamodb actions (eg. scans, projections) it makes sense to use the json document type. If our data is opaque to dynamodb, you are better off with strings.
How can I easily separate JSON values that are sent in the same request?
Given that I POST a JSON to my server:
{"first":"A","second":"B"}
If I implement the following method in the Controller:
#RequestMapping(value = "/path", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public void handleRequest(#RequestBody String input) {
// ...
}
then the input parameter will constitute a String with the entire JSON object, {"first":"A","second":"B"}. What I really want is two separate Strings (or a String and an int whichever is suitable for the particular request) with just the two values (other key / value pairs that the client may send should be ignored).
If the strings were sent as request parameters instead of JSON request body it would be simple:
#RequestMapping(value = "/path", method = RequestMethod.POST)
public void handleRequest(#RequestParam("first") String first,
#RequestParam("second") String second) {
// ...
}
I know that I can create a simple bean class that can be used in conjunction with the #RequestBody annotation that will contain both A and B when used, but it seems like a detour, since they will have different purposes inside the web app.
Dependencies:
org.springframework : spring-web : 3.1.0.RELEASE
org.codehaus.jackson : jackson-mapper-asl : 1.9.3
POJO
public class Input {
private String first;
private String second;
//getters/setters
}
...and then:
public void handleRequest(#RequestBody Input input)
In this case you need Jackson to be available on the CLASSPATH.
Map
public void handleRequest(#RequestBody Map<String, String> input)
I have written a custom WebArgumentResolver that does exactly this, combined with a custom annotation.
I don't have the source available to me now, but basically I annotated my method like this:
#RequestMapping(value = "/path", method = RequestMethod.POST)
public void handleRequest(#JsonField("first") String first, #JsonField("second") String second) {
// ...
}
Then my JsonFieldWebArgumentResolver checks if the method parameter is annotated with JsonField, and if it is it extracts the actual type from the parameter (not quite straight-forward it turns out if you want to handle generic parameters as well, such as List<String> or List<Pojo>), and invokes Jackson's JsonParser manually to create the correct type. It's a shame I can't show you any code, but that's the gist of it.
However, that solution is for Spring MVC 3.0, and if you are using 3.1 I think you will be better off using a custom HandlerMethodArgumentResolver instead. But the idea should be the same.