I want to load any property files in spring POJO classes.
Actually, I want to use any properties from property file in #Size, #NotNull or any validation annotations in spring model classes.
But the issue is that #Size, #NotNull etc annotation are invoked at compile-time whereas any property file's data are invoked at runtime.
Even, I want to use in the max parameter of #Size which accepts the only constant integer. How can I load value and cast it to Integer as a constant value?
And I can use custom validator or any custom annotation to solve the issue but as per the organization policy, I can't use much customization.
I found an additional solution to this problem that we can use a class of constants instead of the property file.
I can use this constants file anywhere in my workspace whereas property file can't use at Size.max property of the model validation
CommonConstants.java
public class CommonConstants
{
public static final int NAME=4;
public static final int ROLE=2;
}
Employee.java
private int id;
#Size(max = CommonConstants.NAME, message = "length exceeds : name")
private String name;
#Size(max = CommonConstants.ROLE, message = "length exceeds : role")
private String role;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
EmployeeController.java
#Controller
public class EmployeeController {
private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
private Map<Integer, Employee> emps = null;
public EmployeeController() {
emps = new HashMap<Integer, Employee>();
}
#RequestMapping(value = "/emp/save.do", method = RequestMethod.POST)
public String saveEmployeeAction(#Valid Employee employee, BindingResult bindingResult, Model model)
{
if (bindingResult.hasErrors()) {
logger.info("Returning empSave.jsp page");
return "empSave";
}
logger.info("Returning empSaveSuccess.jsp page");
model.addAttribute("emp", employee);
emps.put(employee.getId(), employee);
return "empSaveSuccess";
}
}
Output
Related
Let's assume I have many teams like : "Team A, Team B...Team Z" and each team has at least 5 components. Now I want to create a generic controller that responds to any request of type /team/number so I can be able to get informations about a team member.
For example my controller must be able to map this request :
#RequestMapping(value = "/Team A/1", method = RequestMethod.GET)
Team class could be :
#Entity
#Table(name = "team")
public class Team {
public Team() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private List<Player> players;
}
And
Player :
#Entity
#Table(name = "player")
public class Player {
public Player() {}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private int number;
}
Obviously it can either execute GET and POST. The point is that I don't want to specify a controller for each team and each number, I just want one that can respond to /String/int .
I need also to specify which string it can accept and the range of values (max 5 for example).
The #PathVariable spring annotation would help you in this case.
For example:
#RequestMapping(value = "{team}/{component}", method = RequestMethod.GET)
public Team getTeam(#PathVariable String team, #PathVariable int component) {
//Use team and component here
}
You can just add some logic into the controller method to throw some exception if either variable is an unexpected value.
Though depending on what you are trying to achieve, this may be a bad design as this controller method will catch all GET requests that match String/int. You may want to try something like this to make the request mapping more specific:
#RequestMapping(value = "team/{teamLetter}/{component}", method = RequestMethod.GET)
public Team getTeam(#PathVariable char teamLetter, #PathVariable int component) {
//Use team and component here
}
In my spring boot project, I noticed a strange Jackson behavior. I searched over internet, found out what to do, but haven't found out why.
UserDto:
#Setter
#Getter
#AllArgsConstructor
public class UserDto {
private String username;
private String email;
private String password;
private String name;
private String surname;
private UserStatus status;
private byte[] avatar;
private ZonedDateTime created_at;
}
Adding a new user works just fine.
TagDto:
#Setter
#Getter
#AllArgsConstructor
public class TagDto {
private String tag;
}
Trying to add a new tag ends with an error:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of TagDto (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator)
The solution to the problem was to add zero-arg constructor to the TagDto class.
Why does Jackson require no-arg constructor for deserialization in TagDto, while working just fine with UserDto?
Used same method for adding both.
My Tag and User entities are both annotated with
#Entity
#Setter
#Getter
#NoArgsConstructor
and have all args constructors:
#Entity
#Setter
#Getter
#NoArgsConstructor
public class User extends AbstractModel {
private String username;
private String password;
private String email;
private String name;
private String surname;
private UserStatus status;
#Lob
private byte[] avatar;
#Setter(AccessLevel.NONE)
private ZonedDateTime created_at;
public User(final String username, final String password, final String email, final String name, final String surname) {
this.username = username;
this.password = password;
this.email = email;
this.name = name;
this.surname = surname;
this.created_at = ZonedDateTime.now();
}
}
#Entity
#Setter
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class Tag extends AbstractModel {
private String tag;
}
#MappedSuperclass
#Getter
public abstract class AbstractModel {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
}
Entity generation:
#PostMapping(path = "/add")
public ResponseEntity<String> add(#Valid #RequestBody final D dto) {
this.abstractModelService.add(dto);
return new ResponseEntity<>("Success", HttpStatus.CREATED);
}
public void add(final D dto) {
//CRUD repository save method
this.modelRepositoryInterface.save(this.getModelFromDto(dto));
}
#Override
protected Tag getModelFromDto(final TagDto tagDto) {
return new Tag(tagDto.getTag());
}
#Override
protected User getModelFromDto(final UserDto userDto) {
return new User(userDto.getUsername(), userDto.getPassword(), userDto.getEmail(), userDto.getName(), userDto.getSurname());
}
Error occurs when parsing JSON
{"tag":"example"}
sent via postman localhost:8081/tag/add, returns
{
"timestamp": "2020-09-26T18:50:39.974+00:00",
"status": 400,
"error": "Bad Request",
"message": "",
"path": "/tag/add"
}
I am using Lombok v1.18.12 and Spring boot 2.3.3.RELEASE with Jackson v2.11.2.
TL;DR: Solution is at the end.
Jackson supports multiple ways of creating POJOs. The following lists the most common ways, but it likely not a complete list:
Create instance using no-arg constructor, then call setter methods to assign property values.
public class Foo {
private int id;
public int getId() { return this.id; }
#JsonProperty
public void setId(int id) { this.id = id; }
}
Specifying #JsonProperty is optional, but can be used to fine-tune the mappings, together with annotations like #JsonIgnore, #JsonAnyGetter, ...
Create instance using constructor with arguments.
public class Foo {
private int id;
#JsonCreator
public Foo(#JsonProperty("id") int id) {
this.id = id;
}
public int getId() {
return this.id;
}
}
Specifying #JsonCreator for the constructor is optional, but I believe it is required if there is more than one constructor. Specifying #JsonProperty for the parameters is optional, but is required for naming the properties if the parameter names are not included in the class file (-parameters compiler option).
The parameters imply that the properties are required. Optional properties can be set using setter methods.
Create instance using factory method.
public class Foo {
private int id;
#JsonCreator
public static Foo create(#JsonProperty("id") int id) {
return new Foo(id);
}
private Foo(int id) {
this.id = id;
}
public int getId() {
return this.id;
}
}
Create instance from text value using String constructor.
public class Foo {
private int id;
#JsonCreator
public Foo(String str) {
this.id = Integer.parseInt(id);
}
public int getId() {
return this.id;
}
#JsonValue
public String asJsonValue() {
return Integer.toString(this.id);
}
}
This is useful when a the POJO has a simply text representation, e.g. a LocalDate is a POJO with 3 properties (year, month, dayOfMonth), but is generally best serialized as a single string (yyyy-MM-dd format). #JsonValue identifies the method to be used during serialization, and #JsonCreator identifies the constructor/factory-method to be used during deserialization.
Note: This can also be used for single-value construction using JSON values other than String, but that is very rare.
Ok, that was the background information. What is happening for the examples in the question, it that UserDto works because there is only one constructor (so #JsonCreator is not needed), and many arguments (so #JsonProperty is not needed).
However, for TagDto there is only a single-argument constructor without any annotations, so Jackson classifies that constructor as a type #4 (from my list above), not a type #2.
Which means that it is expecting the POJO to be a value-class, where the JSON for the enclosing object would be { ..., "tag": "value", ... }, not { ..., "tag": {"tag": "example"}, ... }.
To resolve the issue, you need to tell Jackson that the constructor is a property initializing constructor (#2), not a value-type constructor (#4), by specifying #JsonProperty on the constructor argument.
This means that you cannot have Lombok create the constructor for you:
#Setter
#Getter
public class TagDto {
private String tag;
public TagDto(#JsonProperty("tag") String tag) {
this.tag = tag;
}
}
In my Spring Boot rest api, I have the following class:
#Entity
#Table(name="Items")
#JsonPropertyOrder({ "itemId", "description", "viewed" })
public class Item {
#ApiModelProperty(notes="Id of the item.", required=true, value="100000")
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#JsonProperty(access=Access.READ_ONLY)
private int itemId = 0;
#ApiModelProperty(notes="Item description.", required=true, value="Item1")
#NotNull
#Size(min=1, max=256)
private String description;
private int viewed;
public int getItemId() {
return this.itemId;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public int getViewed() {
return this.viewed;
}
}
When I execute the request, the JsonPropertyOrder is respected, however, in the Swagger UI (and the Swagger doc), the properties are listed as description, itemId, viewed. I.e. alphabetical. I never turned on alphabetical sorting, so not sure why its doing that... any way to turn that off? It's doing that to all my classes which are laid out in common sense / logical order...
You can define the order in which the properties are going to be shown with ApiModelProperty#position.
Example:
class MyClass {
#ApiModelProperty(position = 0)
String myFirstProperty;
#ApiModelProperty(position = 1)
String mySecondProperty;
}
It's not the most convenient method, but I couldn't find any other way to achieve this...
I'm having this error when trying to compile the project:
Error:Execution failed for task ':app:compileDebugJavaWithJavac'.
java.lang.IllegalArgumentException: intcannot be converted to an Element
And this warning:
Warning:Supported source version 'RELEASE_7' from annotation processor 'android.arch.persistence.room.RoomProcessor' less than -source '1.8'
These are my database related classes:
#Entity(tableName = "users")
public class SerializedUser {
#PrimaryKey(autoGenerate = true)
private int id;
#ColumnInfo(name = "first_name")
private String firstName;
#ColumnInfo(name = "last_name")
private String lastName;
#ColumnInfo(name = "username")
private String username;
public SerializedUser(int id, String firstName, String lastName, String username) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.username = username;
}
public int getId() {
return id;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public String getUsername() {
return username;
}
}
#android.arch.persistence.room.Database(entities = {SerializedUser.class}, version = 4)
public abstract class Database extends RoomDatabase {
public abstract UserDao userDao();
private static final String DATABASE_NAME = "weather";
// For Singleton instantiation
private static final Object LOCK = new Object();
private static volatile Database i;
public static Database init(Context context) {
if (i == null) {
synchronized (LOCK) {
if (i == null) {
i = Room.databaseBuilder(context.getApplicationContext(),
Database.class, Database.DATABASE_NAME)
.fallbackToDestructiveMigration().build();
}
}
}
return i;
}
public static boolean isInited(){
return i != null;
}
public static Database getInstance(){
if(i == null)
throw new NullPointerException("Database.getInstance called when Database not initialized");
return i;
}
}
#Dao
public abstract class UserDao {
Converters converters;
public void inject(Converters converters) {
this.converters = converters;
}
#Insert(onConflict = REPLACE)
public abstract void saveNow(SerializedUser user);
#Delete
public abstract void deleteNow(int id);
#Query("DELETE FROM users")
public abstract void deleteAllNow();
#Query("SELECT * FROM users")
public abstract List<SerializedUser> getAllNow();
#Query("SELECT * FROM users ORDER BY last_name ASC")
public abstract LivePagedListProvider<Integer, SerializedUser> usersByLastName();
}
So the error happens when it tries to implement the database classes? That id on SerializedUser is not the problem, I have commented it out, the problem was still the same. Tried to clean and rebuild the project and restarted Android Studio (invalidate and restart).
remove
#Delete
public abstract void deleteNow(int id);
from your Dao it will work
#Delete annotation marks a method in a Dao annotated class as a
delete method. The implementation of the method will delete its
parameters from the database.
All of the parameters of the Delete method must either be classes annotated with Entity or collections/array of it.
Read here for extra information.
So in your case you pass a parameter with int type which violates the aforementioned rule. That is why you are getting that error.
In order to resolve this issue, you should either exclude deleteNow method or just pass any parameter that does not violates the rule that was mentioned above.
Error:Execution failed for task ':app:compileDebugJavaWithJavac'. java.lang.IllegalArgumentException: intcannot be converted to an Element
Basically, this problem arises not only by the #Delete query, but by all the Room's CRUD annotations (#Insert, #Delete, #Update) except #Query.
All of the parameters of these CRUD annotated methods must either be classes annotated with Entity or collections/array of it.
So we can't pass primitive or other than these.
I have implemented my validation for list of custom class as mention in this post. For reference here my code looks like
class TopDtoForm {
#NotEmpty
private String topVar;
private List<DownDto> downVarList;
//getter and setter
}
class DownDto {
private Long id;
private String name;
//getter and setter
}
#Component
public class TopDtoFormValidator implements Validator {
#Override
public boolean supports(Class<?> clazz) {
return TopDtoForm.class.equals(clazz);
}
#Override
public void validate(Object target, Errors errors) {
TopDtoForm topDtoForm = (TopDtoForm) target;
for(int index=0; index<topDtoForm.getDownVarList().size(); index++) {
DownDto downDto = topDtoForm.getDownVarList().get(index);
if(downDto.getName().isEmpty()) {
errors.rejectValue("downVarList[" + index + "].name", "name.empty");
}
}
}
}
So even I send empty name binding result has 0 error. I tested with topVar and it is working fine. My question is do I have to do any other configuration to say use this validator?
Thanks
In Spring MVC just annotate in TopDtoForm your list with #Valid and add #NotEmpty to DownDto. Spring will validate it just fine:
class TopDtoForm {
#NotEmpty
private String topVar;
#Valid
private List<DownDto> downVarList;
//getter and setter
}
class DownDto {
private Long id;
#NotEmpty
private String name;
//getter and setter
}
Then in RequestMapping just:
#RequestMapping(value = "/submitForm.htm", method = RequestMethod.POST) public #ResponseBody String saveForm(#Valid #ModelAttribute("topDtoForm") TopDtoForm topDtoForm, BindingResult result) {}
Also consider switching from #NotEmpty to #NotBlank as is also checks for white characters (space, tabs etc.)