I'm trying to retrieve data from a database to an mobile app through a REST web service. I've managed to make some basic functions but when I try to add functions I run into problems. For example I want to be able to find "Customers" by their Id and their name. When I have two Get methods, one with "/{id}" and one with "/{name}" the app does not know what to use. What can I do to search by name?
This is the controller from the web service.
package com.example;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
#RestController
#RequestMapping("/customers")
public class CustomerController {
private CustomerRepository repository;
#Autowired
public CustomerController(CustomerRepository repository) {
this.repository = repository;
}
#RequestMapping(value = "/{name}", method = RequestMethod.GET)
public ResponseEntity<Customer> get(#PathVariable("name") String name) {
Customer customer = repository.findByName(name);
if (null == customer) {
return new ResponseEntity<Customer>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Customer>(customer, HttpStatus.OK);
}
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
public ResponseEntity<Customer> get(#PathVariable("id") Long id) {
Customer customer = repository.findOne(id);
if (null == customer) {
return new ResponseEntity<Customer>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<Customer>(customer, HttpStatus.OK);*
}
#RequestMapping(value = "/new", method = RequestMethod.POST)
public ResponseEntity<Customer> update(#RequestBody Customer customer) {
repository.save(customer);
return get(customer.getName());
}
#RequestMapping
public List<Customer> all() {
return repository.findAll();
}
}
This is the service from the android application
package com.ermehtar.poppins;
import java.util.List;
import retrofit2.Call;
import retrofit2.http.Body;
import retrofit2.http.GET;
import retrofit2.http.PATCH;
import retrofit2.http.POST;
import retrofit2.http.Path;
public interface CustomerService {
#GET("customers")
Call<List<Customer>> all();
#GET("customers/{id}")
Call<Customer> getUser(#Path("id") Long id);
#GET("customers/{name}")
Call<Customer> getUser(#Path("name") String name);
#POST("customers/new")
Call<Customer> create(#Body Customer customer);
}
Then this is the function that I use to call the service by name. The response.body will be null when both /name and /id functions are in the web service controller but when one of them is commented out this works just fine.
findUsernameButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Call<Customer> createCall = service.getUser("John");
createCall.enqueue(new Callback<Customer>() {
#Override
public void onResponse(Call<Customer> _, Response<Customer> resp) {
findUsernameButton.setText(resp.body().name);
}
#Override
public void onFailure(Call<Customer> _, Throwable t) {
t.printStackTrace();
allCustomers.setText(t.getMessage());
}
});
}
});
Hope I've made myself understandable. Please ask if there is something unclear or you need more information.
Your restful design can improve. I suggest defining something like this:
New:
/customers/new
This is not correct, in restful a resource creation should be defined by method type. I suggest this:
/customers with POST method.
Search by ID:
/customers/{id}
This is correct, in restful a resource should be access by id using path variable.
Search by name:
/customers/{name}
This is not correct, here you are querying the customers resource, so, you should use query params, I suggest this:
/customers?name=<<name>>
If you have multiple query methods, you will get a conflict because you cannot have more than one GET method in a controller with the same path. So, you can modify #RequestMapping to explicit assert which query params are required like this:
#RequestMapping(value = "/", method = RequestMethod.GET, , params = "name")
public ResponseEntity<Customer> getByName(#RequestParam("name") String name) {
...
}
#RequestMapping(value = "/", method = RequestMethod.GET, , params = "lastname")
public ResponseEntity<Customer> getByLastname(#RequestParam("lastname") String lastname) {
...
}
Differentiate the URLs with a different path which will also make them more RESTful.
Search by name:
/customers/names/{name}
Search by ID:
/customers/ids/{id}
In the future you might want to add another search, perhaps by city:
/customers/cities/{city}
Your controller has ambiguous handler methods mapped so when calling the endpoint you will actually get an exception. Fix this by creating different mappings for get by id and get by name.
Resource is uniquely identified by its PATH (and not by it's params). So, there're few resources with the same PATH: "customers/"
You can create two different resources like:
#RequestMapping(value = "/id", method = RequestMethod.GET)
#RequestMapping(value = "/name", method = RequestMethod.GET)
Or you can make one resource with many request parameters:
#RequestMapping(value = "/get", method = RequestMethod.GET)
public ResponseEntity<Customer> get(#RequestParam ("id") Long id, #RequestParam ("name") String name)
Related
I make application in Java Spring similar to:
Java Blog Aggregator
Author of this app use Spring MVC and common http request. Is it fast way to remodel of controllers to full rest application which use AJAX?
I do not know where to start.
I would like to send JSON. I don't have my own code now, cause I just getting started.
Example of controller:
package cz.jiripinkas.jba.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import cz.jiripinkas.jba.service.ItemService;
#Controller
public class IndexController {
#Autowired
private ItemService itemService;
#RequestMapping("/index")
public String index(Model model) {
model.addAttribute("items", itemService.getItems());
return "index";
}
}
Thanks for any help.
Yes, you can do that, follow these steps:-
Download a JSON API jar and include that library in your application. You can download one HERE
Convert your public String index(Model model) method to public void index(Model model, HttpServletResponse response).
Remove the line return "index"; as now your method is of void type.
Contruct a JSON with the library you just added. You will finally have an object of type JSONObject
Once you have this object, convert and store into a String. For example, if you have a variable named returnJSONObj of type JSONObject, you can write convert it into a String as String returnJSONStr = returnJSONObj.toString()
Finally, you have write the output at a JSON String:-
response.setContentType("application/json");
response.getWriter.println(returnJSONStr);
This will make your method return a JSON String, which you can use in your REST API.
EDIT
Added sample code for reference
package cz.jiripinkas.jba.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import cz.jiripinkas.jba.service.ItemService;
#Controller
public class IndexController {
#Autowired
private ItemService itemService;
#RequestMapping("/index")
public void index(Model model, HttpServletResponse response) {
List items = itemService.getItems();
String returnJSONStr = createJSONStr(items);
response.setContentType("application/json");
response.getWriter().println(returnJSONStr); //this method throws IOException
}
private String createJSONStr(List items) {
JSONObject json = new JSONObject();
for(Item item: items) { //For each item
json.put("property1", item.getProperty1()); //replace by your own properties
json.put("property2", item.getProperty2());
}
return json.toString();
}
}
You can make this a REST Webservice by :
1) Replacing #Controller with #RestController.
2) Changing #RequestMapping("/index") to :
#RequestMapping(value="/index",
method=RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
3) And if you intent to on receiving some payload in this index web service then replace RequestMethod.GET with RequestMethod.POST and change parameters of index function to some DTO object which will map with json you want to receive and annotate it with #RequestBody(This is optional in case you are using #RestController).
EDIT :
4) If you want to return a JSON object , you just need to create a bean with attribute names same as your keys :
class User {
private String id;
private String name;
public void setName(String name){
this.name=name;
}
public void getName(){
return this.name;
}
public void getId(){
return this.id;
}
public void setId(String id){
this.id=id;
}
}
and return this object from the method as follows :
#RequestMapping(value="/index",
method=RequestMethod.GET,
produces=MediaType.APPLICATION_JSON_VALUE)
public User index() {
User user = new User();
user.setId("1");
user.setName("Test");
return user;
}
We are building a webapp that communicates with a remote API. I would like to design the client for this remote API like this:
def RemoteApi
constructor (username, password)
getCurrentUser() //implementation will use username and password
getEmployees() //implementation will use username and password
...
The point being, I want to pass in the credentials to this client during construction, and have all the other methods use these credentials. My second requirement is I want this RemoteApi instance to be in the session.
I have found out how to pass dynamic constructor arguments here.
I have found out how to create a session attribute here.
But I can't figure out a way to combine these two techniques. From what I gather, you have to instantiate a session attribute in its own getter-like method. This getter-like method won't have access to the form's fields so I won't be able to pass in credentials at this point.
Here's where I'm stuck:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import java.util.Map;
#Controller
#SessionAttributes("remoteClient")
public class LoginController {
#Value("${company.name}")
private String companyName;
#Autowired
private RemoteClient remoteClient;
#ModelAttribute("remoteClient")
public RemoteClient addRemoteClientToSession() {
return remoteClient; //how do initialize this with credentials in here?
}
#RequestMapping("/")
public String showLogin(Model model) {
model.addAttribute("credentials", new Credentials());
return "login";
}
#RequestMapping("/login")
public String login(#ModelAttribute("credentials") Credentials credentials, Map<String, Object> model) {
if (remoteClient.auth(companyName, credentials.getUsername(), credentials.getPassword())) {
model.put("fullname", remoteClient.findCurrentUser().getName());
return "projectView";
} else {
return "login";
}
}
}
Update: Maybe the solution has to do with this technique. I didn't know about ModelAndView before this.
The article I linked to in the question did have the necessary technique in it. What I would do is create an #Autowired RemoteApiFactory that has a newInstance(Credentials) method and pass that into addObject. The implementation of newInstance would probably end up using the new keyword. If anyone knows how to avoid this detail, I'd love to hear it.
Here's its example tweaked for my needs:
#Controller
#SessionAttributes("remoteApi")
public class SingleFieldController {
#Autowired
private RemoteApiFactory remoteApiFactory;
#RequestMapping(value="/single-field")
public ModelAndView singleFieldPage() {
return new ModelAndView("single-field-page");
}
#RequestMapping(value="/remember")
public ModelAndView rememberThought(#MethodAttribute("credentials") Credentials credentials) {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("remoteApi", remoteApiFactory.newInstance(credentials));
modelAndView.setViewName("single-field-page");
return modelAndView;
}
}
I would like to achieve something like this with Spring MVC
#RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE)
#RequestMapping(value = "/user/{userId}/delete", method = RequestMethod.POST)
public void deleteUser(#PathVariable String userId) {
...
}
This would give me a common endpoint for REST calls and standard HTML form posts.
Is it possible to do with Spring MVC?
All I can come up with is
#RequestMapping(value = { "/user/{userId}", "/user/{userId}/delete"}, method = {RequestMethod.DELETE, RequestMethod.POST})
public void deleteUser(#PathVariable String userId) {
...
}
but the result is slightly different because a POST to "/user/{userId}" would also delete the user.
One thing you could do is make 2 separate methods with their own RequestMapping annotation, and then just pass the parameters on to a different method, where you do actual stuff:
#RequestMapping(value = "/user/{userId}/delete", method = RequestMethod.POST)
public void deleteUserPost(#PathVariable String userId) {
deleteUser(userId);
}
#RequestMapping(value = "/user/{userId}", method = RequestMethod.DELETE)
public void deleteUserDelete(#PathVariable String userId) {
deleteUser(userId);
}
private void deleteUser(String userId){
//Do things here
}
Sorry, got it wrong way around.
In mature REST architecture, code should use a URL to refer to a resource and use HTTP method to define the action on the resource. So just define a #RequestMapping("/user/{userId}/delete", method = RequestMethod.DELETE) and eliminate the POST. See DELETE vs POST.
I have a basic rest controller taking parameters.
How can I refuse the connection if the query string contains parameters that I did not define?
#RestController
#RequestMapping("/")
public class MyRest {
#RequestMapping(value = "/{id}", method = RequestMethod.GET)
#ResponseBody
public String content(#PathVariable id, #RequestParam(value = "page", required = false) int page) {
return id;
}
}
localhost:8080/myapp/123?pagggge=1
Currently when calling this url, the method is executed with just the id, and the unknown paggge parameter is just neglected. Which is fine in general, but how can I validate them and in case return a HTTP status code?
You may get all parameters incoming and handle in the way you want.
Quoting spring documentation:
When an #RequestParam annotation is used on a Map<String, String> or MultiValueMap<String, String> argument, the map is populated with all request parameters.
In your controller method, you can include an argument of type #RequestParam Map<String, String> to get access to all query parameters passed in. A generic ArgsChecker service class can be used to check whether the user passed in an invalid argument. If so, you can throw an exception, which can be handled by an #ControllerAdvice class.
#RestController
#ExposesResourceFor(Widget.class)
#RequestMapping("/widgets")
public class WidgetController {
#Autowired
ArgsChecker<Widget> widgetArgsChecker;
#RequestMapping(value = "", method = RequestMethod.GET, produces = {"application/hal+json"})
public HttpEntity<PagedResources<WidgetResource>> findAll(#RequestParam #ApiIgnore Map<String, String> allRequestParams, Pageable pageable, PagedResourcesAssembler pageAssembler) {
Set<String> invalidArgs = widgetArgsChecker.getInvalidArgs(allRequestParams.keySet());
if (invalidArgs.size() > 0) {
throw new QueryParameterNotSupportedException("The user supplied query parameter(s) that are not supported: " + invalidArgs + " . See below for a list of query paramters that are supported by the widget endpoint.", invalidArgs, widgetArgsChecker.getValidArgs());
}
The ArgsChecker can be defined as follows:
import com.widgetstore.api.annotation.Queryable;
import lombok.Getter;
import org.apache.commons.lang3.reflect.FieldUtils;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class ArgsChecker<T> {
#Getter
private Set<String> validArgs;
private ArgsChecker(){};
public ArgsChecker(Class<T> someEntityClass){
validArgs= FieldUtils.getFieldsListWithAnnotation(someEntityClass,Queryable.class)
.stream()
.map(Field::getName)
.collect(Collectors.toSet());
validArgs.add("page");
validArgs.add("size");
}
public Set<String> getInvalidArgs(final Set<String> args){
Set<String> invalidArgs=new HashSet<>(args);
invalidArgs.removeAll(validArgs);
return invalidArgs;
}
}
, which uses reflection to find fields which are annotated with the "#Queryable" annotation:
package com.widgetstore.api.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
#Retention(RetentionPolicy.RUNTIME)
public #interface Queryable {
}
Now mark the fields of your domain class which you want queryable with that
annotation:
#Getter
#Setter
public class Widget {
#Queryable
private String productGuid;
#Queryable
private String serialNumber;
private String manufacturer;
Now make sure the ArgsChecker bean is created at application startup:
#SpringBootApplication
public class StartWidgetApi{
public static void main(String[] args){
SpringApplication.run(StartWidgetApi.class);
}
#Bean(name="widgetArgsChecker")
public ArgsChecker<Widget> widgetArgsChecker(){
return new ArgsChecker<Widget>(Widget.class);
}
//Other ArgsCheckers of different types may be used by other controllers.
#Bean(name="fooArgsChecker")
public ArgsChecker<Foo> fooArgsChecker(){
return new ArgsChecker<Foo>(Foo.class);
}
}
Finally,
Define a #ControllerAdvice class which will listen for exceptions thrown by your application:
package com.widgetstore.api.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
#ControllerAdvice
#RequestMapping(produces = "application/json")
#ResponseBody
public class RestControllerAdvice {
#ExceptionHandler(QueryParameterNotSupportedException.class)
public ResponseEntity<Map<String,Object>> unrecogonizedParameter(final QueryParameterNotSupportedException e){
Map<String,Object> errorInfo = new LinkedHashMap<>();
errorInfo.put("timestamp",new Date());
errorInfo.put("errorMessage",e.getMessage());
errorInfo.put("allowableParameters",e.getValidArgs());
return new ResponseEntity<Map<String, Object>>(errorInfo,HttpStatus.BAD_REQUEST);
}
}
, and define the QueryParameterNotSupportedException:
import lombok.Getter;
import java.util.Set;
#Getter
public class QueryParameterNotSupportedException extends RuntimeException{
private Set<String> invalidArgs;
private Set<String> validArgs;
public QueryParameterNotSupportedException(String msg, Set<String> invalidArgs, Set<String> validArgs){
super(msg);
this.invalidArgs=invalidArgs;
this.validArgs=validArgs;
}
}
Now, when the user hits /widgets?someUnknownField=abc&someOtherField=xyz he will get a json response along the lines of
{"timestamp": 2017-01-10'T'blahblah, "errorMessage": "The user supplied query parameter(s) that are not supported: ["someUnknownField","someOtherField"]. See below for a list of allowed query parameters." ,
"allowableParameters": ["productGuid","serialNumber"]
}
Add HttpServletRequest request in method parameters, do
String query = request.getQueryString()
in the method body and validate that.
I wanted to share a different way since I found ametiste's answer too manual and mancini0's overly verbose.
Suppose, you use Spring framework validation API to bind your request parameters to an object ApiRequest.
#InitBinder
public void initBinder(WebDataBinder binder, WebRequest request) {
binder.setAllowedFields(ApiRequest.getAllowedRequestParameters());
}
#RequestMapping("/api")
public String content(#Valid ApiRequest request, BindingResult bindingResult) {
return request.getId();
}
With following ApiRequest definition.
public class ApiRequest {
private String id;
public static String[] getAllowedRequestParameters() {
return new String[]{
"id"
};
}
}
Then you can use the BindingResult to perform the check whether there were any unexpected request parameters, like so:
String[] suppressedFields = bindingResult.getSuppressedFields();
if (suppressedFields.length > 0) {
// your code here
}
I'm trying out Spring MVC 3.0 for the first time and like to make it RESTfull.
This is my controller:
#Controller
#RequestMapping(value = "/product")
#SessionAttributes("product")
public class ProductController {
#Autowired
private ProductService productService;
public void setProductValidator(ProductValidator productValidator, ProductService productService) {
this.productService = productService;
}
#RequestMapping(method = RequestMethod.GET)
public Product create() {
//model.addAttribute(new Product());
return new Product();
}
#RequestMapping(method = RequestMethod.POST)
public String create(#Valid Product product, BindingResult result) {
if (result.hasErrors()) {
return "product/create";
}
productService.add(product);
return "redirect:/product/show/" + product.getId();
}
#RequestMapping(value = "/show/{id}", method = RequestMethod.GET)
public Product show(#PathVariable int id) {
Product product = productService.getProductWithID(id);
if (product == null) {
//throw new ResourceNotFoundException(id);
}
return product;
}
#RequestMapping(method = RequestMethod.GET)
public List<Product> list()
{
return productService.getProducts();
}
}
I have 2 questions about this.
I'm a believer in Convention over Configuration and therefor my views are in jsp/product/ folder and are called create.jsp , list.jsp and show.jsp this works relatively well until I add the #PathVariable attribute. When I hit root/product/show/1 I get the following error:
../jsp/product/show/1.jsp" not found how do I tell this method to use the show.jsp view ?
If I don't add the RequestMapping on class level my show method will be mapped to root/show instead of root/owner/show how do I solve this ? I'd like to avoid using the class level RequestMapping.
add your 'product' to Model and return a String /product/show instead of Product. In your show.jsp, you can access the product object form pageContext
Check out the section in the manual about "Supported handler method arguments and return types".
Basically, when your #RequestMapping method returns just an object, then Spring uses this as a single model attribute, and, I'm guessing, attempts to use the request URL as the basis for the view name.
The easiest way to return the view and data you want from the same method is probably to just have the method return a ModelAndView, so you can explicitly specify the viewName and the model data.