How can correct Ambiguous handler methods mapped? - java

I'm learning about Spring Boot and I have this code :
#GetMapping(value = "test/produits/{prixLimit}")
public List<Product> testeDeRequetes(#PathVariable int prixLimit) {
return productDao.findByPrixGreaterThan(400);
}
#GetMapping(value = "test/produits/{recherche}")
public List<Product> testeDeRequetes(#PathVariable String recherche) {
return productDao.findByNameLike("%"+recherche+"%");
}
The first method is searching with filter.
The second one is searching without filter.
Finally I have this error:
Ambiguous handler methods mapped for '/test/produits/300': {public java.util.List com.ecommerce.microcommerce.web.controller.ProductController.testeDeRequetes(int), public java.util.List com.ecommerce.microcommerce.web.controller.ProductController.testeDeRequetes(java.lang.String)}

I think fundamentally your API is ambiguous. The same verb + path would be confusing to me as a consumer.
It's also a bit limiting. For example, with your setup, you're precluding users from searching for "123" (perhaps it's product ID or SKU).
The prixLimit and recherche parameters seem to be filters/queries on a product resource, so it makes more sense to pass those as query parameters instead of the path:
#GetMapping(value = "test/produits/")
public List<Product> testeDeRequetes(#RequestParam(name = "prixLimit", required = false) Integer prixLimit,
#RequestParam(name = "recherche", required = false) String recherche {
// if prixLimit is not null
// return productDao.findByPrixGreaterThan(prixLimit);
// else if recherche is not null
// return productDao.findByNameLike("%"+recherche+"%");
// else
// return some meaningful default behavior such as all
// products, or return 400 to indicate a bad request
}
But there are a few options to disambiguate if using the path is a required part of this API:
Add an extra Path Element
#GetMapping(value = "test/produits/prixLimit/{prixLimit}")
public List<Product> testeDeRequetes(#PathVariable int prixLimit) {
return productDao.findByPrixGreaterThan(prixLimit);
}
#GetMapping(value = "test/produits/recherche/{recherche}")
public List<Product> testeDeRequetes(#PathVariable String recherche) {
return productDao.findByNameLike("%"+recherche+"%");
}
Use Single Method to Handle Both
#GetMapping(value = "test/produits/{param}")
public List<Product> testeDeRequetes(#PathVariable String param) {
// if param is an int...
// return productDao.findByPrixGreaterThan(param);
// else
// return productDao.findByNameLike("%"+param+"%");
}
Use Regex in Path Mappings
This is still a bit limiting because the two regex patterns must be mutually exclusive or you'll get the same duplicate mapping exception:
// This can only handle digits
#GetMapping(value = "test/produits/{prixLimit:[0-9]+}")
public List<Product> testeDeRequetes(#PathVariable int prixLimit) {
return productDao.findByPrixGreaterThan(400);
}
// This can only handle characters
#GetMapping(value = "test/produits/{recherche:[A-Za-z]+}")
public List<Product> testeDeRequetes(#PathVariable String recherche) {
return productDao.findByNameLike("%"+recherche+"%");
}
Note you cannot search for "abc123" in this scenario.

I suppose you can use regex to make these methods distinct.
#GetMapping(value = "test/produits/{prixLimit:[\\d]+}")
public List<Product> testeDeRequetes(#PathVariable int prixLimit) {
return productDao.findByPrixGreaterThan(400);
}
#GetMapping(value = "test/produits/{recherche:[A-z]}")
public List<Product> testeDeRequetes(#PathVariable String recherche) {
return productDao.findByNameLike("%"+recherche+"%");
}
Here is the example (Section 4.3.#PathVariable with RegEx):
https://www.baeldung.com/spring-requestmapping#3-pathvariable-with-regex

It is possible to use regex
#GetMapping("/products/{id:[\\d]+}")
public List<Product> (#PathVariable Long id){
}
#GetMapping("/products/{name:[a-zA-Z]+}")
public List<Product> (#PathVariable String name){
}

Related

How to apply a recipe on a specific method parameter

In my recipe I select the method parameters which have both the NotNull and RequestParam annotations and I want to apply the OpenRewrite recipe AddOrUpdateAnnotationAttribute on these method parameters to set the required attribute to true of the RequestParam annotation.
I'm struggling how to apply a recipe on a specific piece of code, not on the complete Java class. Does somebody has an example?
An example of the source code before applying my recipe:
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotNull;
class ControllerClass {
public String sayHello (
#NotNull #RequestParam(value = "name") String name,
#RequestParam(value = "lang") String lang
) {
return "Hello";
}
}
The expected source code after applying my recipe:
import org.springframework.web.bind.annotation.RequestParam;
import javax.validation.constraints.NotNull;
class ControllerClass {
public String sayHello (
#NotNull #RequestParam(required = true, value = "name") String name,
#RequestParam(value = "lang") String lang
) {
return "Hello";
}
}
Only the first parameter name needs to be adopted as the second parameter has no NotNull annotation.
My (simplified) recipe:
public class MandatoryRequestParameter extends Recipe {
#Override
public #NotNull String getDisplayName() {
return "Make RequestParam mandatory";
}
#Override
protected #NotNull JavaIsoVisitor<ExecutionContext> getVisitor() {
return new MandatoryRequestParameterVisitor();
}
public class MandatoryRequestParameterVisitor extends JavaIsoVisitor<ExecutionContext> {
#Override
public #NotNull J.MethodDeclaration visitMethodDeclaration(#NotNull J.MethodDeclaration methodDeclaration, #NotNull ExecutionContext executionContext) {
J.MethodDeclaration methodDecl = super.visitMethodDeclaration(methodDeclaration, executionContext);
return methodDeclaration.withParameters(ListUtils.map(methodDecl.getParameters(), (i, p) -> makeRequestParamMandatory(p, executionContext)));
}
private Statement makeRequestParamMandatory(Statement statement, ExecutionContext executionContext) {
if (!(statement instanceof J.VariableDeclarations methodParameterDeclaration) || methodParameterDeclaration.getLeadingAnnotations().size() < 2) {
return statement;
}
AddOrUpdateAnnotationAttribute addOrUpdateAnnotationAttribute = new AddOrUpdateAnnotationAttribute(
"org.springframework.web.bind.annotation.RequestParam", "required", "true", false
);
return (Statement) methodParameterDeclaration.acceptJava(addOrUpdateAnnotationAttribute.getVisitor(), executionContext);
}
}
}
When I execute my recipe, I got following error so my implementation is not the correct way of applying a recipe.
org.openrewrite.UncaughtVisitorException: java.lang.IllegalStateException: Expected to find a matching parent for Cursor{Annotation-\>root}
at org.openrewrite.TreeVisitor.visit(TreeVisitor.java:253)
at org.openrewrite.TreeVisitor.visit(TreeVisitor.java:145)
at org.openrewrite.java.JavaTemplate.withTemplate(JavaTemplate.java:520)
at org.openrewrite.java.JavaTemplate.withTemplate(JavaTemplate.java:42)
at org.openrewrite.java.tree.J.withTemplate(J.java:87)
at org.openrewrite.java.AddOrUpdateAnnotationAttribute$1.visitAnnotation(AddOrUpdateAnnotationAttribute.java:144)
at org.openrewrite.java.AddOrUpdateAnnotationAttribute$1.visitAnnotation(AddOrUpdateAnnotationAttribute.java:78)
at org.openrewrite.java.tree.J$Annotation.acceptJava(J.java:220)
at org.openrewrite.java.tree.J.accept(J.java:60)
at org.openrewrite.TreeVisitor.visit(TreeVisitor.java:206)
at org.openrewrite.TreeVisitor.visitAndCast(TreeVisitor.java:285)
at org.openrewrite.java.JavaVisitor.lambda$visitVariableDeclarations$23(JavaVisitor.java:873)
at org.openrewrite.internal.ListUtils.lambda$map$0(ListUtils.java:141)
at org.openrewrite.internal.ListUtils.map(ListUtils.java:123)
at org.openrewrite.internal.ListUtils.map(ListUtils.java:141)
at org.openrewrite.java.JavaVisitor.visitVariableDeclarations(JavaVisitor.java:873)
at org.openrewrite.java.JavaIsoVisitor.visitVariableDeclarations(JavaIsoVisitor.java:240)
at org.openrewrite.java.JavaIsoVisitor.visitVariableDeclarations(JavaIsoVisitor.java:31)
at org.openrewrite.java.tree.J$VariableDeclarations.acceptJava(J.java:5149)
at org.springframework.sbm.jee.jaxrs.recipes.MandatoryRequestParameter$MandatoryRequestParameterVisitor.makeRequestParamMandatory(MandatoryRequestParameter.java:45)
at org.springframework.sbm.jee.jaxrs.recipes.MandatoryRequestParameter$MandatoryRequestParameterVisitor.lambda$visitMethodDeclaration$0(MandatoryRequestParameter.java:33)
You can get the results you are looking for by using a declarative recipe. You can create a rewrite.yml file in the root of a project and use the rewrite's Maven or Gradle build plugin to apply that recipe.
type: specs.openrewrite.org/v1beta/recipe
name: org.example.MandatoryRequestParameter
displayName: Make Spring `RequestParam` mandatory
description: Add `required` attribute to `RequestParam` and set the value to `true`.
recipeList:
- org.openrewrite.java.AddOrUpdateAnnotationAttribute:
annotationType: org.springframework.web.bind.annotation.RequestParam
attributeName: required
attributeValue: "true"
If using Maven, you can activate that recipe by adding the plugin to your pom.xml:
<plugin>
<groupId>org.openrewrite.maven</groupId>
<artifactId>rewrite-maven-plugin</artifactId>
<version>4.38.0</version>
<configuration>
<activeRecipes>
<recipe>org.example.MandatoryRequestParameter</recipe>
</activeRecipes>
</configuration>
</plugin>
See https://docs.openrewrite.org/getting-started/getting-started for more details on how to use the build plugins.
However, if you want to restrict the change to only specific parameters, while still using the above recipe, you can write an imperative recipe.
public class MandatoryRequestParameter extends Recipe {
private static final String REQUEST_PARAM_FQ_NAME = "org.springframework.web.bind.annotation.RequestParam";
#Override
public #NotNull String getDisplayName() {
return "Make Spring `RequestParam` mandatory";
}
#Override
public String getDescription() {
return "Add `required` attribute to `RequestParam` and set the value to `true`.";
}
#Override
protected TreeVisitor<?, ExecutionContext> getSingleSourceApplicableTest() {
// This optimization means that your visitor will only run if the source file
// has a reference to the annotation.
return new UsesType<>(REQUEST_PARAM_FQ_NAME);
}
#Override
protected #NotNull JavaVisitor<ExecutionContext> getVisitor() {
JavaIsoVisitor addAttributeVisitor = new AddOrUpdateAnnotationAttribute(
REQUEST_PARAM_FQ_NAME, "required", "true", false
).getVisitor();
return new JavaIsoVisitor<ExecutionContext>() {
#Override
public J.Annotation visitAnnotation(J.Annotation annotation, ExecutionContext ctx) {
J.Annotation a = super.visitAnnotation(annotation, ctx);
if (!TypeUtils.isOfClassType(a.getType(), REQUEST_PARAM_FQ_NAME)) {
return a;
}
// The visitor provides a cursor via the `getCusor()` method, and we can use that to navigate
// up to the parent. So, in this case, when we visit the annotation, we can navigate to the
// variable declaration upon which it is defined
J.VariableDeclarations variableDeclaration = getCursor().getParent().getValue();
// This is demonstrating two different ways we might restrict the change:
// - If the parameter's type is a java.lang.Number. Note, this will change all parameters that are
// subtypes of java.lang.Number
// - If the parameter name is equal to "fred"
JavaType paramType = variableDeclaration.getType();
if (TypeUtils.isAssignableTo("java.lang.Number", paramType) ||
variableDeclaration.getVariables().get(0).getSimpleName().equals("fred")) {
// If there is a match, we simple delegate to nested visitor.
return (J.Annotation) addAttributeVisitor.visit(a, ctx, getCursor());
}
return a;
}
};
}
}
}
Here is an example using OpenRewrite's test harness:
#Test
void requiredRequestParam() {
rewriteRun(
spec -> spec
.recipe(new MandatoryRequestParameter())
.parser(JavaParser.fromJavaVersion().classpath("spring-web")),
java(
"""
import org.springframework.web.bind.annotation.RequestParam;
class ControllerClass {
public String sayHello (
#RequestParam(value = "fred") String fred,
#RequestParam(value = "lang") String lang,
#RequestParam(value = "aNumber") Long aNumber
) {
return "Hello";
}
}
""",
"""
import org.springframework.web.bind.annotation.RequestParam;
class ControllerClass {
public String sayHello (
#RequestParam(required = true, value = "fred") String fred,
#RequestParam(value = "lang") String lang,
#RequestParam(required = true, value = "aNumber") Long aNumber
) {
return "Hello";
}
}
"""
)
);
}
Hopefully, this helps!

Multiple Get Requests with Spring Boot, WebClient and Thymeleaf

I'm trying to make a RESTClient application for consuming the Starwars API.
I take the following URL: https://swapi.py4e.com/api/films/
The user logs in my website and can search for a film (introducing the id in a form).
Later, the website shows title, director, release date and a the list of characters.
For instance, if the user inserts 1 (https://swapi.py4e.com/api/films/1), he should get the following info on screen:
{
"title": "A New Hope",
"episode_id": 4,
"director": "George Lucas",
"characters": [
"https://swapi.py4e.com/api/people/1/",
"https://swapi.py4e.com/api/people/2/",
"https://swapi.py4e.com/api/people/3/",
"https://swapi.py4e.com/api/people/4/",
"https://swapi.py4e.com/api/people/5/",
"https://swapi.py4e.com/api/people/6/",
"https://swapi.py4e.com/api/people/7/",
"https://swapi.py4e.com/api/people/8/",
"https://swapi.py4e.com/api/people/9/",
"https://swapi.py4e.com/api/people/10/",
"https://swapi.py4e.com/api/people/12/",
"https://swapi.py4e.com/api/people/13/",
"https://swapi.py4e.com/api/people/14/",
"https://swapi.py4e.com/api/people/15/",
"https://swapi.py4e.com/api/people/16/",
"https://swapi.py4e.com/api/people/18/",
"https://swapi.py4e.com/api/people/19/",
"https://swapi.py4e.com/api/people/81/"
],
The urls from the characters should show the name of the characters.
This should make, I think, an in-between get request to (for example) the urls and stock them in a list, set or array.
For instance: https://swapi.py4e.com/api/people/1/, I only need the name.
{
"name": "Luke Skywalker",
}
For that, I have the following code snippets (I'm using Spring Boot, MVC and working with layers, as in an enterprise application):
DefaultSWAPIClient (with an interface SWAPIClient):
#Component
class DefaultSWAPIClient implements SWAPIClient {
private final WebClient client;
private final String filmURI;
DefaultSWAPIClient(
WebClient.Builder builder,
#Value("${swapi.films}") String filmURI) {
client = builder.build();
this.filmURI = filmURI;
}
#Override
public Optional<Film> findByEpisodeId(long id) {
try {
return Optional.of(
client.get()
.uri(filmURI, uriBuilder -> uriBuilder.build(id))
.retrieve().bodyToMono(Film.class).block());
} catch (WebClientResponseException.NotFound ex) {
return Optional.empty();
}
}
}
DTO (of DAO) film class:
public class Film {
#JsonProperty("title")
private String title;
#JsonProperty("episode_id")
private long episodeId;
#JsonProperty("director")
private String regisseur;
#JsonProperty("release_date")
private String releaseDate;
#JsonProperty("characters")
private LinkedHashSet<String> characters = new LinkedHashSet<String>();
public String getTitle() {
return title;
}
public long getEpisodeId() {
return episodeId;
}
public String getRegisseur() {
return regisseur;
}
public String getReleaseDate() {
return releaseDate;
}
public LinkedHashSet<String> getCharacters() {
return characters;
}
}
The search film Controller (FilmZoekenController):
#Controller
#RequestMapping("filmzoeken")
class FilmZoekenController {
private final SWAPIClient client;
FilmZoekenController(SWAPIClient client) {
this.client = client;
}
#GetMapping
public ModelAndView toonForm() {
return new ModelAndView("filmzoeken");
}
#GetMapping("/film/{id}")
public ModelAndView getData(#PathVariable long id) {
var modelAndView = new ModelAndView("film");
client.findByEpisodeId(id)
.ifPresent(film -> modelAndView.addObject(film));
return modelAndView;
}
}
Then I show all of it in a thymeleaf template.
I don't understand how to make another GET request to the api before it returns the data.
To sum up: I want to do a get request to films/{id} through a form, get some attributes (one being an array of urls, which need to returns the name value from each direction, so I assume another get request within) and show them to the user.
I hope that this should be enough info/code to help me out! Thanks in advance.

I want to send a List(which is a member of an object) from Postman to Spring REST API

I have an object, and of its attributes is a List. I want to send this object from Postman to my service. I'm using Spring 5.2.7 (Spring MVC, not SpringBoot) and Hibernate 5.4.17 and Java 8. My problem is very similar to this one: I want to send a Postman POST request with an Array: members: ["william", "eric", "ryan"]
This is the class I'm trying to pass in Postman (POST method):
public class ChatDescriptionDto {
private String chatID;
private List<String> members;
private String chatType;
public String getChatID() {
return chatID;
}
public void setChatID(String chatID) {
this.chatID = chatID;
}
public List<String> getMembers() {
return members;
}
public void setMembers(List<String> members) {
this.members = members;
}
public void addMembers(List<String> members)
{
if(this.members == null)
this.members = new ArrayList<>();
this.members.addAll(members);
}
public void addMember(String member)
{
if(this.members == null)
this.members = new ArrayList<>();
this.members.add(member);
}
public String getChatType() {
return chatType;
}
public void setChatType(String chatType) {
this.chatType = chatType;
}
}
I've tried this and it didn't work:
{
"chatID": "123",
"members": ["P2001222833","P2001640916"],
"chatType": "personal"
}
Edit: This is my controller:
#PostMapping("/initiateChat")
public String initiateChat(#RequestBody ChatDescriptionDto chat)
{
return chatServiceLocal.initiateChat(chat)?"Chat Description created":"Failure! Could not save.";
}
Edit 2: The method which I've written in the question, "members": ["P2001222833","P2001640916"], is the correct one. Turns out, there was some error in the server so it never started and I didn't check that.
Having no information about the Controller class you're using, the first thing I'd assume is that you're receiving an empty object, which means that Spring simply skipped the serialization. This is the case when you don't specify the parameter of the method as #RequestBody. First, make sure that you do have the annotation.
#RestController
#RequestMapping("/")
public class TestController {
#RequestMapping(value = "/test", method = RequestMethod.POST)
public ResponseEntity test(#RequestBody ChatDescriptionDto dto) {
System.out.println(dto);
return ResponseEntity.ok().build();
}
}
If that's not the case, I'd assume that the problem is with the content type you're using. Spring uses JSON by default, but you can change it in your endpoint's configuration.
To send a simple object request, you do:
{
"member":"kola"
}
To send a list object request, you do:
{
"member": ["kola","wale","ojo"]
}
This is more like listing array elements.
Any error that pops up after this, is basically not because of the request you sent.

How to write a query using only certain parts of an object with Spring JPA

I feel like this should be pretty straightforward, but I'm not sure about the actual code for it. Basically, I have my rest controller taking in 6 arguments, passing that through the Service and then using those arguments to build the object inside of the ServiceImplementation. From there I return a call to my repo using the object I just made. This call should attempt to query the database specific parameters of the object.
This query is the part where I'm not sure how to write using Spring JPA standards. I'd like to just use the variables I set my object with, but I'm not sure if I'll have to write out a query or if spring JPA can make it a bit more simple?
Code:
Controller:
#RestController
public class exampleController {
#Autowired
private ExampleService exampleService;
#GetMapping("/rest/example/search")
public exampleObj searchExample (#RequestParam(value = "exLetter") String exLetter,
#RequestParam(value = "exLang") String exLang, #RequestParam(value = "exType")int exType,
#RequestParam(value = "exMethod") String exMethod, #RequestParam(value = "exCd") String exCd,
#RequestParam(value = "exOrg") String exOrg) {
return exampleService.getExampleLetter(exLetter, exLang, exType, exMethod, exCd, exOrg);
}
}
ExampleSerivce:
public interface ExampleService {
public ExampleLetter getExampleLetter(String exLetter, String exLang, int exType, String exMethod, String exCd, String exOrg);
}
ExampleServiceImplementation:
#Service
public class ExampleServiceImpl implements ExampleService {
#Autowired
private ExampleRepository exampleRepo;
#Override
public ExampleLetter getExampleLetter(String exLetter, String exLang, int exType, String exMethod, String exCd, String exOrg) {
ExampleLetter examp = new ExampleLetter();
examp.setExCd(exCd);
examp.getKey().setExampleNumber(exLetter);
examp.getKey().setLanguageType(exLang);
examp.getKey().setMethod(exMethod);
examp.getKey().setMarketOrg(exOrg);
examp.getKey().setType(exType);
return exampleRepo.findExampleLetter(examp);
}
}
Repo:
#Repository
public interface ExampleRepository extends CrudRepository<ExampleLetter, ExampleLetterKey> {
}
If I understand it correctly, you are trying to make a dinamic query, based on filtering values that may or may not be there. If that's the case, you can use the Specification class to create the query dinamically:
First, in your Repository class, extend JpaSpecificationExecutor<ExampleLetter>:
#Repository
public interface ExampleRepository extends CrudRepository<ExampleLetter, ExampleLetterKey>, JpaSpecificationExecutor<ExampleLetter> {
}
Now, you will need a method (I'd sugest you put it in an specific class, for organization sake) to generate the query itself:
public class GenerateQueryForExampleLetter {
ExampleLetter exampleLetter;
public Specification<ExampleLetter> generateQuery() {
return new Specification<ExampleLetter>() {
private static final long serialVersionUID = 1L;
#Override
public Predicate toPredicate(Root<ExampleLetter> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
Predicate pred = null;
List<Predicate> predicates = new ArrayList<Predicate>();
if (this.exampleLetter.getExCd()!= null && !this.exampleLetter.getExCd().isEmpty()) {
predicates.add(builder.equal(root.<String>get("exCd"), this.exampleLetter.getExCd()));
}
...................
if (this.exampleLetter.getTheFieldYouNeed()!= null && !getTheFieldYouNeed.isEmpty()) {
predicates.add(builder.equal(root.<TheTypeOfTheField>get("theFieldYouNeed"), this.exampleLetter.getTheFieldYouNeed()));
}
if (!predicates.isEmpty()) {
pred = builder.and(predicates.toArray(new Predicate[] {}));
}
return pred;
}
};
}
public void setExampleLetter (ExampleLetter el) {
this.exampleLetter = el;
}
}
Finally, in your service class:
#Override
public ExampleLetter getExampleLetter(String exLetter, String exLang, int exType, String exMethod, String exCd, String exOrg) {
ExampleLetter examp = new ExampleLetter();
examp.setExCd(exCd);
examp.getKey().setExampleNumber(exLetter);
examp.getKey().setLanguageType(exLang);
examp.getKey().setMethod(exMethod);
examp.getKey().setMarketOrg(exOrg);
examp.getKey().setType(exType);
GenerateQueryForExampleLetter queryGenerator = new GenerateQueryForExampleLetter ();
queryGenerator.setExampleLetter(examp);
return exampleRepo.findAll(queryGenerator.generateQuery());
}
Note that the JpaSpecificationExecutor interface adds a few utility methods for you to use which, besides filtering, supports sorting and pagination.
For more details, check here, here, or this answer.

How to programmatically replace Spring's NumberFormatException with a user-friendly text?

I am working on a Spring web app and i have an entity that has an Integer property which the user can fill in when creating a new entity using a JSP form. The controller method called by this form is below :
#RequestMapping(value = {"/newNursingUnit"}, method = RequestMethod.POST)
public String saveNursingUnit(#Valid NursingUnit nursingUnit, BindingResult result, ModelMap model)
{
boolean hasCustomErrors = validate(result, nursingUnit);
if ((hasCustomErrors) || (result.hasErrors()))
{
List<Facility> facilities = facilityService.findAll();
model.addAttribute("facilities", facilities);
setPermissions(model);
return "nursingUnitDataAccess";
}
nursingUnitService.save(nursingUnit);
session.setAttribute("successMessage", "Successfully added nursing unit \"" + nursingUnit.getName() + "\"!");
return "redirect:/nursingUnits/list";
}
The validate method simply checks if the name already exists in the DB so I did not include it. My issue is that, when I purposely enter text in the field, I would like to have a nice message such as "The auto-discharge time must be a number!". Instead, Spring returns this absolutely horrible error :
Failed to convert property value of type [java.lang.String] to required type [java.lang.Integer] for property autoDCTime; nested exception is java.lang.NumberFormatException: For input string: "sdf"
I fully understand why this is happening but i cannot for the life of me figure out how to, programmatically, replace Spring's default number format exception error message with my own. I am aware of message sources which can be used for this type of thing but I really want to achieve this directly in the code.
EDIT
As suggested, i built this method in my controller but i'm still getting Spring's "failed to convert property value..." message :
#ExceptionHandler({NumberFormatException.class})
private String numberError()
{
return "The auto-discharge time must be a number!";
}
OTHER EDIT
Here is the code for my entity class :
#Entity
#Table(name="tblNursingUnit")
public class NursingUnit implements Serializable
{
private Integer id;
private String name;
private Integer autoDCTime;
private Facility facility;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
#Size(min = 1, max = 15, message = "Name must be between 1 and 15 characters long")
#Column(nullable = false, unique = true, length = 15)
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
#NotNull(message = "The auto-discharge time is required!")
#Column(nullable = false)
public Integer getAutoDCTime()
{
return autoDCTime;
}
public void setAutoDCTime(Integer autoDCTime)
{
this.autoDCTime = autoDCTime;
}
#ManyToOne (fetch=FetchType.EAGER)
#NotNull(message = "The facility is required")
#JoinColumn(name = "id_facility", nullable = false)
public Facility getFacility()
{
return facility;
}
public void setFacility(Facility facility)
{
this.facility = facility;
}
#Override
public boolean equals(Object obj)
{
if (obj instanceof NursingUnit)
{
NursingUnit nursingUnit = (NursingUnit)obj;
if (Objects.equals(id, nursingUnit.getId()))
{
return true;
}
}
return false;
}
#Override
public int hashCode()
{
int hash = 3;
hash = 29 * hash + Objects.hashCode(this.id);
hash = 29 * hash + Objects.hashCode(this.name);
hash = 29 * hash + Objects.hashCode(this.autoDCTime);
hash = 29 * hash + Objects.hashCode(this.facility);
return hash;
}
#Override
public String toString()
{
return name + " (" + facility.getCode() + ")";
}
}
YET ANOTHER EDIT
I am able to make this work using a message.properties file on the classpath containing this :
typeMismatch.java.lang.Integer={0} must be a number!
And the following bean declaration in a config file :
#Bean
public ResourceBundleMessageSource messageSource()
{
ResourceBundleMessageSource resource = new ResourceBundleMessageSource();
resource.setBasename("message");
return resource;
}
This gives me the correct error message instead of the Spring generic TypeMismatchException / NumberFormatException which i can live with but still, I want to do everything programmatically wherever possible and I'm looking for an alternative.
Thank you for your help!
You may be able to override that messaging by providing an implementation of the Spring DefaultBindingErrorProcessor similar to what is done here:
Custom Binding Error Message with Collections of Beans in Spring MVC
You can annotate a method with:
#ExceptionHandler({NumberFormatException.class})
public String handleError(){
//example
return "Uncorrectly formatted number!";
}
and implement whatever you want to do in case the exception of that type is thrown. The given code will handle exceptions happened in the current controller.
For further reference consult this link.
To make global error handling you can use #ControllerAdvice in the following way:
#ControllerAdvice
public class ServiceExceptionHandler extends ResponseEntityExceptionHandler {
#ExceptionHandler({NumberFormatException.class})
public String handleError(){
//example
return "Uncorrectly formatted number!";
}
}
#Martin, I asked you about the version because #ControllerAdvice is available starting with version 3.2.
I would recommend you to use #ControllerAdvice, which is an annotation that allows you to write code that is sharable between controllers(annotated with #Controller and #RestController), but it can also be applied only to controllers in specific packages or concrete classes.
ControllerAdvice is intended to be used with #ExceptionHandler, #InitBinder, or #ModelAttribute.
You set the target classes like this #ControllerAdvice(assignableTypes = {YourController.class, ...}).
#ControllerAdvice(assignableTypes = {YourController.class, YourOtherController.class})
public class YourExceptionHandler{
//Example with default message
#ExceptionHandler({NumberFormatException.class})
private String numberError(){
return "The auto-discharge time must be a number!";
}
//Example with exception handling
#ExceptionHandler({WhateverException.class})
private String whateverError(WhateverException exception){
//do stuff with the exception
return "Whatever exception message!";
}
#ExceptionHandler({ OtherException.class })
protected String otherException(RuntimeException e, WebRequest request) {
//do stuff with the exception and the webRequest
return "Other exception message!";
}
}
What you need to keep in mind is that if you do not set the target and you define multiple exception handlers for the same exceptions in different #ControllerAdvice classes, Spring will apply the first handler that it finds. If multiple exception handlers are present in the same #ControllerAdvice class, an error will be thrown.
Solution 1: StaticMessageSource as Spring bean
This gives me the correct error message instead of the Spring generic TypeMismatchException / NumberFormatException which i can live with but still, I want to do everything programmatically wherever possible and I'm looking for an alternative.
Your example uses ResourceBundleMessageSource which uses resource bundles (such as property files). If you want to use everything programmatically, then you could use a StaticMessageSource instead. Which you can then set as a Spring bean named messageSource. For example:
#Configuration
public class TestConfig {
#Bean
public MessageSource messageSource() {
StaticMessageSource messageSource = new StaticMessageSource();
messageSource.addMessage("typeMismatch.java.lang.Integer", Locale.getDefault(), "{0} must be a number!");
return messageSource;
}
}
This is the simplest solution to get a user friendly message.
(Make sure the name is messageSource.)
Solution 2: custom BindingErrorProcessor for initBinder
This solution is lower level and less easy than solution 1, but may give you more control:
public class CustomBindingErrorProcessor extends DefaultBindingErrorProcessor {
public void processPropertyAccessException(PropertyAccessException ex, BindingResult bindingResult) {
Throwable cause = ex.getCause();
if (cause instanceof NumberFormatException) {
String field = ex.getPropertyName();
Object rejectedValue = ex.getValue();
String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field);
Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field);
boolean useMyOwnErrorMessage = true; // just so that you can easily see to default behavior one line below
String message = useMyOwnErrorMessage ? field + " must be a number!" : ex.getLocalizedMessage();
FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true, codes, arguments, message);
error.wrap(ex);
bindingResult.addError(error);
} else {
super.processPropertyAccessException(ex, bindingResult);
}
}
}
#ControllerAdvice
public class MyControllerAdvice {
#InitBinder
public void initBinder(WebDataBinder binder) {
BindingErrorProcessor customBindingErrorProcessor = new CustomBindingErrorProcessor();
binder.setBindingErrorProcessor(customBindingErrorProcessor);
}
}
It basically intercepts the call to DefaultBindingErrorProcessor.processPropertyAccessException and adds a custom FieldError message when binding failed with a NumberFormatException.
Example code without Spring Web/MVC
In case you want to try it without Spring Web/MVC, but just plain Spring, then you could use this example code.
public class MyApplication {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MyConfig.class);
Validator validator = context.getBean(LocalValidatorFactoryBean.class);
// Empty person bean to be populated
Person2 person = new Person2(null, null);
// Data to be populated
MutablePropertyValues propertyValues = new MutablePropertyValues(List.of(
new PropertyValue("name", "John"),
// Bad value
new PropertyValue("age", "anInvalidInteger")
));
DataBinder dataBinder = new DataBinder(person);
dataBinder.setValidator(validator);
dataBinder.setBindingErrorProcessor(new CustomBindingErrorProcessor());
// Bind and validate
dataBinder.bind(propertyValues);
dataBinder.validate();
// Get and print results
BindingResult bindingResult = dataBinder.getBindingResult();
bindingResult.getAllErrors().forEach(error ->
System.out.println(error.getDefaultMessage())
);
// Output:
// "age must be a number!"
}
}
#Configuration
class MyConfig {
#Bean
public LocalValidatorFactoryBean validator() {
return new LocalValidatorFactoryBean();
}
}
class Person2 {
#NotEmpty
private String name;
#NotNull #Range(min = 20, max = 50)
private Integer age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
public Person2(String name, Integer age) {
this.name = name;
this.age = age;
}
}
Handle NumberFormatException.
try {
boolean hasCustomErrors = validate(result, nursingUnit);
}catch (NumberFormatException nEx){
// do whatever you want
// for example : throw custom Exception with the custom message.
}

Categories

Resources