Requirement is to write a controller to handle POST requests from the below urls:
http://hostname:port/com/prod1?id=2&action=add
http://hostname:port/com/prod1?id=2&action=minus
http://hostname:port/com/prod2?id=2&action=add
http://hostname:port/com/prod2?id=2&action=minus
Can I have two methods, one for mapping urls with action=add, and another for urls with action=minus? All the requests are POST.
A couple years between question and answer, but yes it is possible to route based on query parameters with Spring MVC:
#RequestMapping(path = "/com/{product}", params = "add")
String add(#PathVariable("product") String product) {
System.out.println("add method; product=" + product);
return "add";
}
#RequestMapping(path = "/com/{product}", params = "minus")
String minus(#PathVariable("product") String product) {
System.out.println("minus method; product=" + product);
return "minus";
}
No to my knowledge. According to your url end points,
they are,
/com/prod1
/com/prod2
For these you can have 2 controller methods for each of these.
EDIT:
If I understand you correctly,
Instead of having the above end points, write 2 controller methods for request mappings,
com/add
com/minus
If you want to have it in a more granular manner, then
com/prod1/add
com/prod1/minus
com/prod2/add
com/prod2/minus
write request mapping methods for the above.
Then you have,
http://hostname:port/com/prod1/add?id=2
http://hostname:port/com/prod1/minus?id=2
http://hostname:port/com/prod2/add?id=2
http://hostname:port/com/prod2/munus?id=2
Or you can use another approach. which is, Just use the generic end points and depending on your request parameters, redirect to different request mappings such as,
com/add
com/minus
You cannot have two #RequestMapping methods to map to two URLs differing only by query parameters. The #RequestMapping only binds to the path portion of the URL.
You can have two methods if you dispatch on the action value, calling one method for add and another for minus.
Or, you can make add or minus a part of the URL path.
It is possible with the following code:
#GetMapping(path = "/com/{product}", params = "action=add")
public String add(#PathVariable("product") String product) {
System.out.println("add method; product=" + product);
return "add";
}
#GetMapping(path = "/com/{product}", params = "action=minus")
public String minus(#PathVariable("product") String product) {
System.out.println("minus method; product=" + product);
return "minus";
}
Related
Using Mockito version 4.8.0
The controller method I need to test
#GetMapping(value = "getStringBuiltByComplexProcess")
public String getStringBuiltByComplexProcess(#RequestParam String firstName, #RequestParam String lastName ) {
Author a = new Author();
return a.methodWhichMakesNetworkAndDatabaseCalls(firstName, lastName);
}
here is the test method
#Test
public void testGetStringBuiltByComplexProcess01() {
final String firstName = "firstName";
final String lastName = "lastName";
try (MockedConstruction<Author> mock = mockConstruction(Author.class)) {
Author authorMock = new Author();
when(authorMock.methodWhichMakesNetworkAndDatabaseCalls(eq(firstName), eq(lastName))).thenReturn("when worked");
assertEquals("when worked", ut.getStringBuiltByComplexProcess(firstName, lastName), "Strings should match");
verify(authorMock).methodWhichMakesNetworkAndDatabaseCalls(eq(firstName), eq(lastName));
}
}
fails with a message of
org.opentest4j.AssertionFailedError: strings should match ==> expected: <when worked> but was: <null>
In this simplified example the controller method has more code but the core of what is not working is mocking the object which the controller method constructs.
The object you create on line
Author authorMock = new Author();
is different than the one created in the getBooksByAuthor() function. A debugger should show you that.
You can use mock.constructed().get(0) to get the object created in getBooksByAuthor(), but by the time you can do this, getBooksByAuthor() has already finished and you can't do much with that mock.
It's not exactly clear what your objective is. I guess you want to check that the Author object is created in a certain way, and the lines involving getFullName() aren't part of the actual code, just something you added to experiment, because they don't do anything.
If you want to verity that the object passed to dataAccessService satisfies some conditions, what you need is an ArgumentCaptor. Something like
ArgumentCaptor<Author> authorCaptor = ArgumentCaptor.forClass(Author.class);
when(dataAccessServiceMock.getBooks(authorCaptor.capture())).thenReturn(books);
List<Book> result = ut.getBooksByAuthor(firstName, lastName);
Author author = authorCaptor.value();
assertEquals(firstName, author.getFirstName());
If you use MockInitializer for stubbing , it should solve your problem :
try (MockedConstruction<Author> mocked = mockConstruction(Author.class, (mock, context) -> {
when(authorMock.methodWhichMakesNetworkAndDatabaseCalls(eq(firstName), eq(lastName))).thenReturn("when worked");
})) {
assertEquals("when worked", ut.getStringBuiltByComplexProcess(firstName, lastName), "Strings should match");
verify(authorMock).methodWhichMakesNetworkAndDatabaseCalls(eq(firstName), eq(lastName));
}
}
But a much better way to test the controller is to use MockMvc. It allows you to test for a given HTTP request , do you configure spring-mvc properly such that it can do the following things correctly :
if the HTTP request can be parsed properly to execute the expected controller method with the expected paramater
if it can deserialize the object returned from the controller method into a expected JSON structure.
it allows to configure the current user who make the HTTP call and verify if he has enough permission to call this API and if not, will it return the expected error response
etc.
All of these things cannot be tested by your fragile mocking constructor approach.
For more details about MockMvc, refer to this guide.
Context:
I want to write an endpoint that will return a Collection of users based on their usernames. How should those username be passed to the REST endpoint - note that I can (potentially) have a lot of usernames (say > 5000)?
Solution #1:
Use a GET endpoint, concatenate the usernames on client side and pass them as a single request parameter. Split the request parameter on server side to get the list of usernames.
#RestController
public class UserController {
#GetMapping
// able to deserialize `filename1,filename2` to List out of the box
public Collection<User> getUser(#RequestParam List<String> usernames) {
return userService.getUsersByUsername(usernames);
}
}
Solution #2:
Use a POST endpoint and pass the list of usernames as request body. Although cleaner form a coding perspective, I end up using a POST to fetch data.
#RestController
public class UserController {
#PostMapping
public Collection<User> getUser(#RequestBody List<String> usernames) {
return userService.getUsersByUsername(usernames);
}
}
Questions:
Which of the two solutions would be the better approach?
Do you have a better approach to pass the list of usernames into the endpoint?
Edits:
I've updated the signature of the first solution based on suggestions from answers. Spring is able to deserialize filename1,filename2 to List out of the box for #RequestParam.
POST looks like a cleaner approach in this case because -
Sending a huge string in a URL is not a good idea and there is scope for error
You need to write additional code (logic) to create the string on frontend and split it on backend.
Sending a huge string in a URL is not scalable as there are limits on the length of URL.
Get approach might result into an issue since URL length is limited and then you have to limit your query parameters.
Though its not a post request but in your case i think post is the only way out.
I would agree with all the answers given above. I would like to specify one more point , if you are going with post request you might have to increase the payload capacity a server can receive , the default post capacity(The maximum size in bytes) of spring boot is 2mb (based on your server). While testing your code might work fine with 1000-2000 usernames but make sure to change the property to accept more bytes in request.
GET is not limited, yet the browser is. Your server client does not seem to be the browser, so I would say GET is the way to go.
P.S GET can receive a body (not so great, but POST is not also the best match).
You don need to concatenated the string and add extra computation on server server, GET can receive a list of separate strings.
UPDATE with example:
#RestController
public class MyController {
#GetMapping(value = "/test")
public List<String> getTestParams(#RequestParam List<String> params) {
return params;
}
}
The test with 3000 params
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class TestMyController {
#Autowired
private TestRestTemplate restTemplate;
#Test
public void testRequestWithParamsList() {
List<String> params = new ArrayList<>();
for (int i = 0; i < 3000; i++) {
params.add(String.valueOf(i));
}
List<String> result = restTemplate.getForObject(buildUrl(params),
List.class);
assertEquals(params, result);
}
private String buildUrl(List<?> params) {
return "/test?params=" + getUrlParameter(params);
}
private String getUrlParameter(List<?> params) {
return params.stream()
.map(Object::toString)
.collect(Collectors.joining(","));
}
}
If you are using tomcat you must specify also the max http header property in application.properties
server.max-http-header-size=30000
Here is a scenario.
Case1: (#QueryParam("username") String username)
URL: example?username=yourname
Case2: (#QueryParam("username") String name)
URL: example?username=yourname
In these 2 cases which is correct way of using QueryParam.
When I use Case1, it works, yourname is printed. But when I use Case2, null is printed.
I want to implement Case 2 because of following some coding standards. Means I do not want to change the variable name (which is "String name"). But in URL I have to use "username".
Is there any way of using Case2 scenario for QueryParam.
Adding code(1st edit)
Here is the code which I replicated the issue that I am facing
#RequestMapping (value = "/username1", method = RequestMethod.GET)
#ResponseBody
public Response username1(#QueryParam("username") String username) {
System.out.println("Username1 is ==> " + username);
return Response.ok(username).build();
}
#RequestMapping (value = "/username2", method = RequestMethod.GET)
#ResponseBody
public Response username2(#QueryParam("username") String name) {
System.out.println("Username2 is ==> " + name);
return Response.ok(name).build();
}
/username1?username=yourname
Output: Username1 is ==> yourname
/username2?username=yourname
Output: Username2 is ==> null
Thank you
Yes, you're doing something wrong: you're using QueryParam, which is a JAX-RS annotation, in a Spring-MVC application.
The equivalent Spring annotation is RequestParam. JAX-RS and Spring MVC are two different things. You can't just use the annotations of one in the other.
I have to implement the same endpoint that return different data type. I can't use versioning in the URL so just wondering how I can use header content to map request to based on header value.
For example I will be sending val1 or val2 in header say decider. And I want to have controller methods like
#RequestMapping(value = "\someUrl")
public firstReturnType someMethod() {
}
#RequestMapping(value = "\someUrl")
public secondReturnType someOtherMethod() {
}
Any suggestion?
This is the HTTP Headers list: https://en.wikipedia.org/wiki/List_of_HTTP_header_fields
you can see that you don't have in HTTP headers that you can use to your needs or any other custom needs.
I would recommend differentiating between the 2 methods by adding HTTP verb, GET, POST, DELETE, PUT.
Or just change your URL to:
#RequestMapping(value = "/someUrl/{id}")
Or just use one method and use if statement to decide which answer to return:
#RequestMapping(value = "/someUrl")
public firstReturnType someMethod(#RequestParam("param") int param ) {
if(param == 1){
...
}
else if(param == 2){
...
}
}
I am trying to receive a list of String as comma separated value in the REST URI ( sample :
http://localhost:8080/com.vogella.jersey.first/rest/todo/test/1/abc,test
, where abc and test are the comma separated values passed in).
Currently I am getting this value as string and then splitting it to get the individual values.
Current code :
#Path("/todo")
public class TodoResource {
// This method is called if XMLis request
#GET
#Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
#Path("/test/{id: .*}/{name: .*}")
public Todo getXML(#PathParam("id") String id,
#PathParam("name") String name) {
Todo todo = new Todo();
todo.setSummary("This is my first todo, id received is : " + id
+ "name is : " + Arrays.asList(name.split("\\s*,\\s*")));
todo.setDescription("This is my first todo");
TodoTest todoTest = new TodoTest();
todoTest.setDescription("abc");
todoTest.setSummary("xyz");
todo.setTodoTest(todoTest);
return todo;
}
}
Is there any better method to achieve the same?
I am not sure what you are trying to achieve with your service, however, it may be better to use query parameters to get multiple values for a single parameter. Consider the below URL.
http://localhost:8080/rest/todos?name=name1&name=name2&name=name3
And here is the code snippet for the REST service.
#GET
#Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
#Path("/todos")
public Response get(#QueryParam("name") List<String> names) {
// do whatever you need to do with the names
return Response.ok().build();
}
If you don't know how many comma separated values you will get, then the split you do is as far as I've been able to find the best way to do it. If you know you will always have 3 values as comma separated, then you can get those 3 directly. (for instance if you have lat,long or x,y,z then you could get it with 3 pathvariables. (see one of the stackoverflow links posted below)
there are a number of things you can do with matrix variables but those require ; and key/value pairs which is not what you're using.
Things I found (apart from the matrix stuff)
How to pass comma separated parameters in a url for the get method of rest service
How can I map semicolon-separated PathParams in Jersey?