The following controller serves a simple html page showing all persons in a repository.
Problem: I'm using validation constraints on the get-query. And if the query was invalid (in my example: lastname parameter is missing), then spring automatically throws an exception as response to the browser.
But I'd still want to render the persons.html page, just showing the errors indead of the repository content.
Question: how could I achieve this? Because if the validation fails, the method below is not even accessed.
#Controller
#RequestMapping("/persons")
public class PersonController {
#GetMapping //note: GET, not POST
public String persons(Model model, #Valid PersonForm form) {
//on the persons.html page I want to show validation errors
model.addAttribute("persons", dao.findAll());
return "persons";
}
}
public class PersonForm {
private String firstname;
#NotBlank
private String lastname;
}
Sidenote: I'm using thymeleaf as templating engine. But the same question would apply to jsp or jsf engine.
Adding BindingResult should solve this problem as #obecker pointed. I saw your remark, it works for GetMapping and #PostMapping as well.
Please check this out:
#SpringBootApplication
public class So45616063Application {
public static void main(String[] args) {
SpringApplication.run(So45616063Application.class, args);
}
public static class PersonForm {
private String firstname;
#NotBlank
private String lastname;
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
#Override
public String toString() {
return firstname + " " + lastname;
}
}
#RestController
#RequestMapping("/")
public static class Home {
#GetMapping
public void get(#Valid PersonForm form, BindingResult bindingResult) {
System.out.println(form);
System.out.println(bindingResult);
}
}
}
Call:
curl -XGET 'localhost:8080?firstname=f&lastname=l'
Will produce output:
f l
org.springframework.validation.BeanPropertyBindingResult: 0 errors
Call:
curl -XGET 'localhost:8080?firstname=f'
Will produce:
f null
org.springframework.validation.BeanPropertyBindingResult: 1 errors
You need an additional BindingResult bindingResult parameter in the persons method. You can use this bindingResult to see whether there are validation errors.
Spring has a nice guide that shows how to do this.
See https://spring.io/guides/gs/validating-form-input/
Related
In my Spring Boot application I am creating a REST API, which is calling some other external REST API. I created User class, which is a object that is received by my Rest API downloaded from the external API. My user model looks like:
#JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String fullName;
private String department;
#JsonGetter("fullName")
public String getFullName() {
return fullName;
}
#JsonSetter("full_name")
public void setFullName(String fullName) {
this.fullName = fullName;
}
#JsonGetter("department")
public String getDepartment() {
return department;
}
#JsonSetter("department")
public void setDepartment(String department) {
this.department = department;
}
}
I am using JsonGetter and JsonSetter properties, because I would like to have my json properties in response returned in camelCase, but the properties given in external API are returned with underscore:
External API Response:
{
"full_name": "User A",
"department": "A",
}
My API Response:
{
"fullName": "User A",
"department": "A",
}
And everything seems to be working fine (hitting my API with Postman gives proper responses) until I started to create some Http request tests. In tests I receive assertion error that fullName property is null, while doing the same request in postman is responding with proper responses.
My test class:
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class HttpRequestTest {
#LocalServerPort
private int port;
#Autowired
private TestRestTemplate restTemplate;
#Test
public void shouldReturnUserFullName() throws Exception {
assertThat(this.restTemplate.getForObject("http://localhost:" + port + "/users/a",
User.class)).extracting(User::getFullName)
.contains("User A");
}
}
My controller method:
#GetMapping("users/{name}")
public ResponseEntity<User> getSpecificUserByName(#PathVariable("name") String name) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
HttpEntity<?> entity = new HttpEntity<>(headers);
ResponseEntity<User> response = restTemplate.exchange(createUriString(name), HttpMethod.GET, entity, User.class);
return response;
}
Test result:
java.lang.AssertionError:
Expecting:
<[null]>
to contain:
<["User A"]>
but could not find:
<["User A"]>
I would appreciate any help with this issue :)
#JsonSetter("full_name") expects your API response to contain a property full_name during deserialzation. Since #JsonGetter("fullName") converts full_name to fullName, field private String fullName; is never set.
You should change #JsonSetter("full_name") to #JsonSetter("fullName").
Let us take an example
Suppose your REST API returns below Object of User class
User reponse = new User();
response.setFullName("User A");
response.setDepartment("A");
So, when we call your REST API, the JSON response would look like as below
{
"fullName":"User A",
"department":"A"
}
Now, When you pass this JSON to convert into User class, Jackson will look for methods with the name setFullName and setDepartment.
In your test case, something similar is happening,
for code
this.restTemplate.getForObject("http://localhost:" + port + "/users/a",User.class)
First, it calls your API to get the User object Serialized and then it Deserialized it to User class. While Deserializing, it looks for a method named
setFullName without any
#Setter
#JsonProperty
#JsonAlias
annotations
or will look for any setter method with
#Setter("fullName")
#JsonProperty("fullName"),
#JsonAlias("fullName")
but in your case, the fullName setter is treated as
public void setFull_name(String fullName) {
this.fullName = fullname;
}
So, setter for fullName is not found but since you marked your User class as
#JsonIgnoreProperties(ignoreUnknown = true)
hence any exception is not thrown but fullName for your Response JSON is ignored, so fullName is never set, which remains null and your Test case is failing.
So, either change your test case or mark your setter with
#JsonAlias("fullName")
annotation.
i.e. Your User class will look like as below
#JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String fullName;
private String department;
#JsonGetter("fullName")
public String getFullName() {
return fullName;
}
#JsonAlias({"fullName","full_name"})
public void setFullName(String fullName) {
this.fullName = fullName;
}
#JsonGetter("department")
public String getDepartment() {
return department;
}
#JsonSetter("department")
public void setDepartment(String department) {
this.department = department;
}
}
So I am wondering what the best way to process an edit request based on a user role.
Say I have the following PostMapping:
#PostMapping(value = "/edit")
public ResponseEntity<String> editIoc(#RequestBody GeneralPojoAllFields editRequest)
the GeneralPojoAllFields looks like this:
public class GeneralPojoAllFields {
private String firstName;
private String lastName;
private String onlyAdminCanEditField;
}
This is the pojo the the admin will be able to use and that will eventually get mapped into the entity class to be saved to the database. However, if we have a regular user who wants to edit it and hypothetically they aren't restricted in the UI would that design work? What I am currently thinking is I would have a user pojo like so:
public class UserPojo {
private String firstName;
private String lastName;
}
After the request mapping comes we check if the user is either regular user or an admin. If it is a regular user we just map the GeneralPojoAllFields to the UserPojo and it wont map over the onlyAdminCanEditField and continue from there.
Is there a better way to do this?
First, your backend should be as independent of the UI as possible. So, access control in UI is a good to have design, but you should not depend upon it.
Now, coming back to your question, yes you can use SecurityContextHolder to find out if the user if regular user/admin. However, if its possible, I would suggest making two controllers, one for admin and one for regular user. Use #PreAuthorize on the admin controller to restrict access. Having two separate controllers will increase readability of your code tremendously.
Additionally, you can call the same service class method from both the controllers. And since you already have two POJO classes, you can use them in #RequestBody and let Spring take care of the mappings for you.
Well, it depends what you think a better way would be. It also depends a bit on your data source. But as there is no information on that here, I would suggest that a better way to do yours is by inheritance.
Make UserPojo the super class and GeneralPojoAllFields extend that class.
UserPojo.java:
public class UserPojo {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public UserPojo() {}
public UserPojo(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
GeneralPojoAllFields.java:
public class GeneralPojoAllFields extends UserPojo {
private String onlyAdminCanEditField;
public String getOnlyAdminCanEditField() {
return onlyAdminCanEditField;
}
public void setOnlyAdminCanEditField(String onlyAdminCanEditField) {
this.onlyAdminCanEditField = onlyAdminCanEditField;
}
public GeneralPojoAllFields() {}
public GeneralPojoAllFields(String firstName, String lastName, String onlyAdminCanEditField) {
super(firstName, lastName);
this.onlyAdminCanEditField = onlyAdminCanEditField;
}
}
App.java:
public class App {
public static void main(String[] args) {
UserPojo up1 = new UserPojo();
up1.setFirstName("MyFirstName");
up1.setLastName("MyLastName");
GeneralPojoAllFields gpaf1 = new GeneralPojoAllFields();
gpaf1.setFirstName("MyFirstName");
gpaf1.setLastName("MyLastName");
gpaf1.setOnlyAdminCanEditField("yes");
}
}
I was trying to use Spring Data JPA on Spring Boot and I kept getting error, I can't figure out what the problem is:
Unable to locate Attribute with the the given name [firstName] on
this ManagedType [com.example.h2demo.domain.Subscriber]
FirstName is declared in my entity class. I have used a service class with DAO before with different project and worked perfectly.
My Entity class (getters and setters are also in the class) :
#Entity
public class Subscriber {
#Id #GeneratedValue
private long id;
private String FirstName,LastName,Email;
public Subscriber(long id, String firstName, String lastName, String email) {
this.id = id;
this.FirstName = firstName;
this.LastName = lastName;
this.Email = email;
}
}
...
My Repository Class
#Component
public interface SubscriberRepository extends JpaRepository<Subscriber,Long> {
Subscriber findByFirstName(String FirstName);
Subscriber deleteAllByFirstName(String FirstName);
}
My Service Class
#Service
public class SubscriberService {
#Autowired
private SubscriberRepository subscriberRepository;
public Subscriber findByFirstName(String name){
return subscriberRepository.findByFirstName(name);
}
public Subscriber deleteAllByFirstName(String name){
return subscriberRepository.deleteAllByFirstName(name);
}
public void addSubscriber(Subscriber student) {
subscriberRepository.save(student);
}
}
And My Controller class:
#RestController
#RequestMapping("/subscribers")
public class SubscriberController {
#Autowired
private SubscriberService subscriberService;
#GetMapping(value = "/{name}")
public Subscriber findByFirstName(#PathVariable("name") String fname){
return subscriberService.findByFirstName(fname);
}
#PostMapping( value = "/add")
public String insertStudent(#RequestBody final Subscriber subscriber){
subscriberService.addSubscriber(subscriber);
return "Done";
}
}
Try changing private String FirstName,LastName,Email; to private String firstName,lastName,email;
It should work.
findByFirstName in SubscriberRepository tries to find a field firstName by convention which is not there.
Further reference on how properties inside the entities are traversed https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-property-expressions
The same problem was when i had deal with Spring Data Specifications (https://www.baeldung.com/rest-api-search-language-spring-data-specifications)
Initial piece of code was:
private Specification<Project> checkCriteriaByProjectNumberLike(projectNumber: String) {
(root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("project_number"), "%" + projectNumber)
}
The problem was in root.get("project_number"). Inside the method, I had to put the field name as in the model (projectNumber), but I sent the field name as in the database (project_number).
That is, the final correct decision was:
private Specification<Project> checkCriteriaByProjectNumberLike(projectNumber: String) {
(root, query, criteriaBuilder) -> criteriaBuilder.like(root.get("projectNumber"), "%" + projectNumber)
}
After I change my entity class variables from capital letter to small letter for instance Username to username the method Users findByUsername(String username); is working for me now .
As per specification , the property names should start with small case.
...The resolution algorithm starts with interpreting the entire part (AddressZipCode) as the property and checks the domain class for a property with that name (uncapitalized)....
It will try to find a property with uncapitalized name. So use firstName instead of FristName and etc..
I am following this spring guide:
https://spring.io/guides/gs/accessing-mongodb-data-rest/
Everything is perfect, however if I want to POST a document with manual id, I am not able to do that.
Here is what all I have done:
I inserted one document from Mongo shell by the command db.person.insert({"_id": "111111", "firstName" : "Vikas", "lastName" : "Prasad"});
This works fine and if I do a GET at http://localhost:8080/people from Postman, I can see the person document with id 111111 in the response having self href as http://localhost:8080/people/111111
But if I am sending a POST request from Postman at http://localhost:8080/people with body as {"_id": "222222", "firstName" : "Aadish", "lastName" : "Patodi"}, the document is getting inserted with an auto id instead of 222222. Because of which obviously I cant access this docuemnt by doing a GET at http://localhost:8080/people/222222 unlike the case when I used insert() from the shell to insert a document with manual id. Instead I have to hit a GET at http://localhost:8080/people/57bc29ada3fab115cc9b546b to fetch this second document.
Just to check if I am POSTing the {"_id": "222222", "firstName" : "Aadish", "lastName" : "Patodi"} again, its getting inserted again at a new auto generated id: http://localhost:8080/people/57bc2bdaa3fab115cc9b546c. It means MongoDB is not even looking at the _id, else it must have thrown duplicate key error.
I tried searching various sources. All I can found is an implementation of the data access code separately in JAVA at back end and calling respective MongoDB methods.
My question is:
Just like in the given tutorial they are performing every operation without defining any JAVA back end code for data access from MongoDB for auto id documents, is there a way to do the same for manual id documents?
Or just for this one use case I have to implement the data access code at the back end?
I am using CorsFilter to handle cross origin requests.
Edit:
Below is the Person class:
package hello;
import org.springframework.data.annotation.Id;
public class Person {
#Id private String id;
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
I have tried:
-> adding getter and setter for id attribute
-> renaming id to employeeNumber
-> renaming id to employeeNumber and adding getter and setter for employeeNumber
None of the above three solved the issue.
as discussed on the comment, looks like your _id field is not mapped correctly. Can you check if the _id is mapped correctly in the pojo ?
Finally, I got it working by renaming id with _id and adding getter and setter for the same in the Person class.
package hello;
import org.springframework.data.annotation.Id;
public class Person {
#Id private String _id;
private String firstName;
private String lastName;
public String get_id() {
return _id;
}
public void set_id(String _id) {
this._id = _id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
I'm using Spring to implement a RESTful web service. One of the endpoints takes in a JSON string as request body and I wish to map it to a POJO. However, it seems right now that the passed-in JSON string is not property mapped to the POJO.
here's the #RestController interface
#RequestMapping(value="/send", headers="Accept=application/json", method=RequestMethod.POST)
public void sendEmails(#RequestBody CustomerInfo customerInfo);
the data model
public class CustomerInfo {
private String firstname;
private String lastname;
public CustomerInfo() {
this.firstname = "first";
this.lastname = "last";
}
public CustomerInfo(String firstname, String lastname)
{
this.firstname = firstname;
this.lastname = lastname;
}
public String getFirstname(){
return firstname;
}
public void setFirstname(String firstname){
this.firstname = firstname;
}
public String getLastname(){
return lastname;
}
public void getLastname(String lastname){
this.lastname = lastname;
}
}
And finally my POST request:
{"CustomerInfo":{"firstname":"xyz","lastname":"XYZ"}}
with Content-Type specified to be application/json
However, when I print out the object value, the default value("first" and "last") got printed out instead of the value I passed in("xyz" and "XYZ")
Does anyone know why I am not getting the result I expected?
FIX
So it turned out that, the value of request body is not passed in because I need to have the #RequestBody annotation not only in my interface, but in the actual method implementation. Once I have that, the problem is solved.
So it turned out that, the value of request body is not passed in because I need to have the #RequestBody annotation not only in my interface, but in the actual method implementation. Once I have that, the problem is solved.
You can do it in many ways, Here i am going to do it in below different ways-
NOTE: request data shuld be {"customerInfo":{"firstname":"xyz","lastname":"XYZ"}}
1st way We can bind above data to the map as below
#RequestMapping(value = "/send", headers = "Accept=application/json", method = RequestMethod.POST)
public void sendEmails(#RequestBody HashMap<String, HashMap<String, String>> requestData) {
HashMap<String, String> customerInfo = requestData.get("customerInfo");
String firstname = customerInfo.get("firstname");
String lastname = customerInfo.get("lastname");
//TODO now do whatever you want to do.
}
2nd way we can bind it directly to pojo
step 1 create dto class UserInfo.java
public class UserInfo {
private CustomerInfo customerInfo1;
public CustomerInfo getCustomerInfo1() {
return customerInfo1;
}
public void setCustomerInfo1(CustomerInfo customerInfo1) {
this.customerInfo1 = customerInfo1;
}
}
step 1. create another dto classCustomerInfo.java
class CustomerInfo {
private String firstname;
private String lastname;
public String getFirstname() {
return firstname;
}
public void setFirstname(String firstname) {
this.firstname = firstname;
}
public String getLastname() {
return lastname;
}
public void setLastname(String lastname) {
this.lastname = lastname;
}
}
step 3 bind request body data to pojo
#RequestMapping(value = "/send", headers = "Accept=application/json", method = RequestMethod.POST)
public void sendEmails(#RequestBody UserInfo userInfo) {
//TODO now do whatever want to do with dto object
}
I hope it will be help you out. Thanks
The formatting on this is terrible, but this should work for jackson configuration.
<!-- Use Jackson for JSON conversion (POJO to JSON outbound). -->
<bean id="jsonMessageConverter"
class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
<!-- Use JSON conversion for messages -->
<bean class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jsonMessageConverter"/>
</list>
</property>
</bean>
ALso, as mentioned in a comment, your JSON is wrong for your object.
{"firstname":"xyz","lastname":"XYZ"}
does appear to be the correct JSON for your object.
Sample Data :
[
{
"targetObj":{
"userId":1,
"userName":"Devendra"
}
},
{
"targetObj":{
"userId":2,
"userName":"Ibrahim"
}
},
{
"targetObj":{
"userId":3,
"userName":"Suraj"
}
}
]
For above data this pring controller method working for me:
#RequestMapping(value="/saveWorkflowUser", method = RequestMethod.POST)
public void saveWorkflowUser (#RequestBody List<HashMap<String ,HashMap<String ,
String>>> userList ) {
System.out.println(" in saveWorkflowUser : "+userList);
//TODO now do whatever you want to do.
}
remove those two statements from default constructor and try