How to check spring RestController for unknown query params? - java

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
}

Related

Passing a class using template

I'm created a interface to abstract some common methods on 2 classes, here is the code
package br.canabarro.resource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Repository;
#Repository
public interface BaseResource<T, ID> {
ResponseEntity save( T entity);
ResponseEntity delete(ID id);
ResponseEntity getAll();
ResponseEntity getById(ID id);
ResponseEntity update(ID id, T entity);
}
I'm have another file called ProductResource
#Override
#RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity save(#RequestBody DtoProduct productDTO) {
final Product product = new Product(productDTO.getName());
return ResponseEntity.status(HttpStatus.ACCEPTED).body(productService.save(product));
}
The save method are implementend on this function, but the compiler returns Method does not override method from its superclass, but if i change the function parameter to Product productParam will pass, the class Product have and id and name on the parameters.
The last part is my ProductToDto and this method also implement a interface called SuperConverter
package br.canabarro.dto.Conversor;
import br.canabarro.dto.DtoProduct;
import br.canabarro.entity.Category;
import br.canabarro.entity.Product;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
#Component
public class ProductToDto implements SuperConverter<Product, DtoProduct> {
#Override
public DtoProduct apply(Product product) {
List<String> categoriesID = product
.getCategories()
.stream()
.map(Category::getId)
.collect(Collectors.toList());
return DtoProduct
.build()
.id(product.getId())
.name(product.getName())
.builder();
}
}
How can i change the BaseResoucer to make java understand what i want is passing a template T and not the class name like DtoProduct.
I'm sorry if the question became so long but this is a important question for me.
On the pastebin i'm posting my SuperConverter and the DtoProduct
SuperConverter: https://pastebin.com/TmFRLQVz
DtoProduct: https://pastebin.com/VTSnMVhq
ProductResource is defined as:
public class ProductResource implements BaseResource<Product, String>
So Product becomes your T in BaseResource. In such case it is expected that ProductResource implements the following method:
ResponseEntity save(Product entity);
And it does not do that.
You would need to define your ProductResource as:
public class ProductResource implements BaseResource<DtoProduct, String>
or if you want to keep it as Product then implement the following method:
ResponseEntity save(Product entity);
It is not really Spring issue but Java generics. Have a look at https://docs.oracle.com/javase/tutorial/java/generics/index.html

How to have to different GET methods in Controller Java Spring

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)

Convert Spring MVC to Spring fullREST JSON application

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;
}

Cannot resolve ResponseEntity in SpringMVC using JpaRepository

This is my Controller:
package com.hodor.booking.controller;
import com.hodor.booking.jpa.domain.Vehicle;
import com.hodor.booking.service.VehicleService;
import com.wordnik.swagger.annotations.Api;
import org.apache.commons.lang.time.DateUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
#RestController
#RequestMapping("/api/v1/vehicles")
#Api(value = "vehicles", description = "Vehicle resource endpoint")
public class VehicleController {
private static final Logger log = LoggerFactory.getLogger(VehicleController.class);
#Autowired
private VehicleService vehicleService;
#RequestMapping(method = RequestMethod.GET)
public List<Vehicle> index() {
log.debug("Getting all vehicles");
return vehicleService.findAll();
}
#RequestMapping(value="/save", method=RequestMethod.POST, consumes="application/json")
#ResponseBody
public Vehicle setVehicle(#RequestBody Vehicle vehicle) {
log.debug("Inserting vehicle");
if (vehicle.getLicensePlate() == null){
return new ResponseEntity<Void>(HttpStatus.CONFLICT);
}
return vehicleService.saveVehicle(vehicle);
}
}
What I want to achieve in above If-Guard is that, in case the vehicle Object does not have the LicensePlate Member, send back an according HTTP Status Header CONFLICT or something.
I am coming from a Node and Express background and I am used to set my header, send the response and be done with it. However in this case (JPA) it does not seem to work. Any ideas?
One alternative is to take advantage of Spring's validation support to declarative add validation of your POJOs. Basically, you add annotations to your Vehicle class like:
public class Vehicle {
#NotNull
private LicensePlate licensePlate;
// getters, setters
}
And you add a #Valid annotation to your controller method:
#ResponseBody
public Vehicle setVehicle(#RequestBody #Valid Vehicle vehicle) {
log.debug("Inserting vehicle");
return vehicleService.saveVehicle(vehicle);
}
If the validation fails, Spring will return a 400 response.
Make sure that you have JSR-303/JSR-349 Bean Validation implementation such as the Hibernate Validator (it can be used without Hibernate's ORM support) on your classpath.
More information can be found in the validation chapter of the Spring reference docs.
What version of Spring MVC are you using? From another post How to respond with HTTP 400 error in a Spring MVC #ResponseBody method returning String?. It states that Spring MVC 4.1 and later using different syntax.

In Spring-MVC, how do I dynamically set constructor arguments of a session attribute?

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;
}
}

Categories