How to write or configure generic validation annotation or class for request param and request body instead of validating one by one
The below example we are validating one by one in the method itself. Instead of that how to validate generic way which we will use for all the methods and class rather than validating each method
#PostMapping(value="/agreements/{agreement-number}/payments", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<ResponseModel> initiatePayment(#Valid #RequestBody PaymentInitiateModel paymentInitiateModel,
#RequestHeader(value = "orgCode", required = true) #Valid #NotNull(message = "Mandatory Field Missing") #Size(min = 3, max = 10, message = "Min/Max Length Validation Failed") String orgCode,
#RequestHeader(value = "apiName", required = true) #Valid #NotNull(message = "Mandatory Field Missing") #Size(min = 3, max = 20, message = "Min/Max Length Validation Failed") String apiName,
#RequestHeader(value = "uniqueRefNo", required = true) #Valid #NotNull(message = "Mandatory Field Missing") #Size(min = 3, max = 15, message = "Min/Max Length Validation Failed") String uniqueRefNo,
#RequestHeader(value = "requestTime", required = true) #ValidDate #NotNull(message = "Mandatory Field Missing") String requestTime,
#RequestHeader(value = "status", required = true) #Valid #NotNull(message = "Mandatory Field Missing") #Size(min = 1, max = 10, message = "Min/Max Length Validation Failed") String status,
#RequestHeader(value = "remarks", required = true) #Valid #NotNull(message = "Mandatory Field Missing") #Size(min = 3, max = 10, message = "Min/Max Length Validation Failed") String remarks,
#PathVariable("agreement-number") String agreementNumber)
{
You can create data classes and give validation constraints in those classes. It will look something like the following.
public class ExampleRequest {
#NotBlank(message = "app.user.full.name.invalid")
#Size(max = 128, message = "invalid.field.size.128")
private String fullName;
#NotBlank(message = "invalid.format.phone.number")
#Size(min = 11, max = 11, message = "invalid.field.size.128")
private String phoneNumber;
#NotBlank(message = "app.user.mail.invalid")
#Size(max = 128, message = "invalid.field.size.128")
private String mail;
#NotBlank(message = "app.user.password.invalid")
#Size(max = 128, message = "invalid.field.size.128")
private String password;
#NotBlank(message = "app.user.password.confirm.invalid")
#Size(min = 8, max = 32, message = "password.character.count.invalid")
private String passwordConfirm;
#NotNull(message = "app.user.role.names.null")
private Set<String> roleNames;
private Long companyId = -1L;
}
Then use it with #Valid annotation and it would work.
public ResponseEntity<ResponseModel> exampleMethod(#RequestBody #Valid ExampleRequest exampleRequest) {
// ...
}
Related
I have two entities. A vulnerability can have multiple vulnerability identifiers.
#Entity
#JsonInclude(Include.NON_NULL)
#ApiModel(parent = ApprovableEntity.class)
public class Vulnerability {
...
#JsonProperty("vulnerabilityIdentifiers")
#JoinColumn(name = "vulnerabilityidentifier_id")
#JsonView(JsonViews.BasicChangeLogView.class)
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<VulnerabilityIdentifier> vulnerabilityIdentifiers;
...
}
#Entity
#ApiModel(parent = ApprovableEntity.class)
public class VulnerabilityIdentifier {
...
#ManyToOne
#JoinColumn(name = "vulnerability_id", referencedColumnName = "id")
#NotNull(message = "vulnerability is required")
#JsonView({JsonViews.BasicApprovableView.class, JsonViews.BasicChangeLogView.class,
JsonViews.ChangeLogAnswerView.class, JsonViews.DraftAnswerView.class})
#ApiModelProperty(hidden = true)
private Vulnerability vulnerability;
#Column(name = "type")
#JsonProperty("type")
#Size(max = 12)
#NotNull(message = "CVEID type required")
#ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String cveIdType;
#Column(name = "value")
#JsonProperty("value")
#Size(max = 24)
#NotNull(message = "value is required")
#ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY)
private String value;
...
}
Now when I send in a json request to the endpoint like as under, the application throws exception that it cannot map the type and value fields in the vulnerabilityIdentifier field.
A sample json request
{
"vulnerabilityImpacts": {
},
"vulnerabilityIdentifiers": [{"type": "cveId", "value": "CVE-1234-12345"}],
"vulnerableThreeppcomponents": [],
"internalSource": "**",
"cveId": "*****",
......
}
Both the cveId and value properties are annotated with #ApiModelProperty(accessMode = ApiModelProperty.AccessMode.READ_ONLY) which means they are ignored when deserializing. Remove this annotation from both properties.
I have a model class:
#Builder
#Data
#Entity
#AllArgsConstructor
#NoArgsConstructor
public class Employee {
#GeneratedValue(strategy = GenerationType.AUTO)
#Type(type="uuid-char")
#Column(updatable = false, nullable = false, unique = true)
#Id
private UUID id;
#Column(updatable = true, nullable = false, unique = true)
#Email(message = "Enter a valid email")
private String email;
#NotNull(message = "First name cannot be empty")
#Size(min = 3, message = "First name character must be more than 3!")
private String firstName;
#Size(min = 3, message = "Last name character must be more than 3!")
private String lastName;
#Range(min = 21, max = 55, message = "Age must be between 21 and 55")
private int age;
#JsonIgnore
private Double accBalance;
#NotNull(message = "Gender cannot be empty")
private String gender;
#NotNull(message = "Country cannot be empty")
private String country;
#JsonProperty("Job Scope")
private String designation;
#CreationTimestamp
private Date createdAt;
#DateTimeFormat
private Date birthDate;
}
And this is my test class:
class EmployeeTest {
#Test
public void testObjectMethod() {
Employee object = new Employee();
object.equals(new Employee());
object.hashCode();
object.toString();
}
#Test
public void testAll() {
Employee object = new Employee();
object.equals(Employee.builder().build());
}
}
And this is my coverage. Basically it only covers 73.8%. What other tests do I need to do to achieve 100%? As this covers quite a lot and doesn't need much of thinking, I would like to target 100%. Appreciate any help or pointers.
coverage
You need to do following
write test for equals
write test for hashcode
write test case for constructor no-arg and all arg
test case for setter and getter for all attribute
you can write assertNotNull for hashCode various tests.
I have 2 tables. One of them called 'products'
#Data
#Entity
#Table(name = "products")
#NoArgsConstructor
public class Product {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(length = 100)
#NotBlank(message = "Name must be written")
private String name;
#Column(length = 200)
#NotBlank(message = "Provide image (link in this case) of your product")
private String image;
#PositiveOrZero
private int amount;
#Column(length = 250)
#NotBlank(message = "description must be written")
#Size(min = 10, max = 250, message = "description is too long or empty")
private String description;
#PositiveOrZero
private float price;
#ManyToOne
#JoinColumn(name = "type_id")
private ProductType productType;
public Product(#NotBlank String name, String image, int amount, #NotBlank String description,
#PositiveOrZero float price, ProductType productType) {
this.name = name;
this.image = image;
this.amount = amount;
this.description = description;
this.price = price;
this.productType = productType;
}
}
another table is 'users'
#Data
#Entity
#NoArgsConstructor
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#Column(length = 50)
#Size(min = 2, max = 30, message = "enter appropriate amount of letters, min 2")
private String username;
#Column(length = 100)
#Email(message = "Enter a valid email")
#NotBlank(message = "email should have a value")
private String email;
#Column(length = 50)
#NotBlank(message = "password should have a value")
#Size(min = 6, message = "password should at least consist of 6 characters")
private String password;
private boolean enabled;
private String role;
public User(#Size(min = 2, max = 30, message = "enter appropriate amount of letters, min 2")
String username,
#Email(message = "Enter a valid email")
#NotBlank(message = "email should have a value") String email,
#NotBlank(message = "password should have a value")
#Size(min = 6, message = "password should at least consist of 6 characters")
String password, boolean enabled, String role) {
this.username = username;
this.email = email;
this.password = password;
this.enabled = enabled;
this.role = role;
}
}
and also table that include both 'product_user' (many to many relationship)
it looks like this
#Data
#Entity
#Table(name = "product_user")
#AllArgsConstructor
#NoArgsConstructor
public class ProdAndUser{
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Integer id;
#ManyToOne
#JoinColumn(name = "product_id")
Product product;
#ManyToOne
#JoinColumn(name = "user_id")
User user;
public ProdAndUser(Product product, User user) {
this.product = product;
this.user = user;
}
}
then I tried to get them from prodAndUser repository by UserId or by User as obj:
#Repository
public interface ProdAndUserRepository extends JpaRepository<ProdAndUser, Integer> {
List<ProdAndUser> getProdAndUsersByUserId(Integer id);
List<ProdAndUser> getAllByUser(User user);
}
my controller looks like this:
#ResponseBody
#GetMapping("/findByUsr/{user}")
public List<ProdAndUser> getByUser(#PathVariable User user){
return prodAndUserRepository.getAllByUser(user);
}
error:
{
"timestamp": "2022-02-12T05:52:53.165+00:00",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/Cart/findByUsr"
}
I have tried to find them all by .findAll() and it worked fine. Also another tables work fine on their own
Look at the error it says (404) means something is not right with the path.
The path on the error does not contain user_id.
In my Spring project I'm using Springdoc to generate a OpenApiSpecification doc.
I created my Api with these annotations. I want to have the same endpoint url with different mediatype to handle the POST of different objects.
#Validated
#Tag(name = "Calendar", description = "Api for Calendar resource")
public interface CalendarApi {
#Operation(summary = "Add an appointment to the calendar", description = "Add an appointment to the calendar", tags = {"appointment"})
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Successful operation", content = #Content(mediaType = "application/json+widget", schema = #Schema(implementation = AppointmentWidgetDto.class))),
#ApiResponse(responseCode = "400", description = "Invalid input")
})
#PostMapping(value = "/appointments", consumes = "application/json+widget")
ResponseEntity<Appointment> saveFromWidget(#Parameter(description = "The new appointment to save", required = true) #Valid #RequestBody AppointmentWidgetDto appointmentDto);
#Operation(summary = "Add an appointment to the calendar", description = "Add an appointment to the calendar", tags = {"appointment"})
#ApiResponses(value = {
#ApiResponse(responseCode = "201", description = "Successful operation", content = #Content(mediaType = "application/json", schema = #Schema(implementation = Appointment.class))),
#ApiResponse(responseCode = "400", description = "Invalid input")
})
#PostMapping(value = "/appointments", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Appointment> save(#Parameter(description = "The new appointment to save", required = true) #Valid #RequestBody Appointment appointmentDto);
}
The generated Open Api Spec document is:
/api/v1/appointments:
post:
tags:
- Calendar
summary: Add an appointment to the calendar
description: Add an appointment to the calendar
operationId: save_1
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Appointment'
application/json+widget:
schema:
$ref: '#/components/schemas/AppointmentWidgetDto'
required: true
responses:
'201':
description: Successful operation
content:
application/json:
schema:
$ref: '#/components/schemas/Appointment'
'400':
description: Invalid input
content:
'*/*':
schema:
$ref: '#/components/schemas/Appointment'
I've a couple of porblems:
the endpoint name is not meaningful (save_1)
when I use Open Api generator to generate the Angular client from this specification, I've some warnings that prevent the generation of both methods.
[WARNING] Multiple schemas found in the OAS 'content' section, returning only the first one (application/json)
[WARNING] Multiple MediaTypes found, using only the first one
I know there is this issue opened (https://github.com/OpenAPITools/openapi-generator/issues/3990).
Is there any way to permit to POST two different bodies in the same endpoint url and using OpenApi generator to create client for different languages/platforms?
===== UPDATE =======
This is AppointmentWidgetDTO:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#SuperBuilder
public class AppointmentWidgetDto implements Serializable {
#NotNull(message = "{appointment.store.missing}")
#JsonDeserialize(using = StoreUriDeserializer.class)
private Store store;
#NotNull(message = "{appointment.title.missing}")
#Size(max = 255)
private String title;
#Lob
#Size(max = 1024)
private String description;
#Size(max = 50)
private String type;
#Size(max = 50)
private String icon;
#NotNull(message = "{appointment.startdate.missing}")
private Instant startDate;
#NotNull(message = "{appointment.enddate.missing}")
private Instant endDate;
#JsonDeserialize(using = ContactUriDeserializer.class)
private Contact contact;
#NotBlank(message = "{appointment.contactname.missing}")
private String contactName;
#NotBlank(message = "{appointment.email.missing}")
#Email
private String contactEmail;
#NotBlank(message = "{appointment.phone.missing}")
#PhoneNumber
private String contactPhone;
}
and this is Appointment:
#ScriptAssert(lang = "javascript", script = "_.startDate.isBefore(_.endDate)", alias = "_", reportOn = "endDate", message = "{appointment.invalid.end.date}")
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#SuperBuilder
public class Appointment extends AbstractEntity {
#NotNull(message = "{appointment.store.missing}")
#JsonDeserialize(using = StoreUriDeserializer.class)
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "store_id", updatable = false)
private Store store;
#NotNull
#Size(max = 255)
#Column(nullable = false, length = 255)
private String title;
#Lob
#Size(max = 1024)
#Column(length = 1024)
private String description;
#Size(max = 30)
#Column(length = 30)
private String color;
#Size(max = 50)
#Column(length = 50)
private String type;
#Size(max = 50)
#Column(length = 50)
private String icon;
#Size(max = 255)
#Column(length = 255)
private String location;
#NotNull
#Column(nullable = false)
private Instant startDate;
#NotNull
#Column(nullable = false)
private Instant endDate;
#Builder.Default
#NotNull
#Column(nullable = false, columnDefinition = "BIT DEFAULT 0")
private boolean allDay = false;
#JoinColumn(name = "contact_id")
#JsonDeserialize(using = ContactUriDeserializer.class)
#ManyToOne(fetch = FetchType.LAZY)
private Contact contact;
private String contactName;
#Email
private String contactEmail;
#PhoneNumber
private String contactPhone;
#JoinColumn(name = "agent_id")
#JsonDeserialize(using = AgentUriDeserializer.class)
#ManyToOne(fetch = FetchType.LAZY)
private Agent agent;
private String agentName;
#Builder.Default
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
#NotNull
#Column(nullable = false)
#Enumerated(EnumType.STRING)
private AppointmentStatus status = AppointmentStatus.VALID;
With OpenAPI 3, you can not have many operations for the same path.
You will have only one endpoint and only one OpenAPI description.
What you can do is to define the #Operation annotation on the top of one of the methods, where you add the OpenAPI documentation of the merged OpenAPI description of all your other methods as well and add the #Hidden annotation on the others.
Or you can define two different groups: For each one you filter using header matching, option headersToMatch of GroupedOpenApi Bean.
In the current project I am working on the model bean has the phone number in 3 fields but we only would like one spring validation error. Can this be done>
#NotEmpty(message = "Your Question must not be blank.")
#Size(min = 10, max = 200)
private String content;
#NotEmpty(message = "Area Code must not be blank.")
#Size(min = 3, max = 3, message = "Area must be 3 numbers")
private String areacode;
#NotEmpty(message = "Phone Number must not be blank.")
#Size(min = 3, max = 3, message = "phone number first part must be 3 numbers")
private String phone3;
#NotEmpty(message = "Phone Number must not be blank.")
#Size(min = 4, max = 4, message = "Phone number last part must be 4 numbers")
private String phone4;
If it does not matter whenever the validation error occur at class level, but not at the field, then you can use hibernate validations ScriptAssert Validation
#ScriptAssert(
lang="javascript"
script="_this.areacode!=null and _this.phone3!=null and _this.phone4!=null"
)
public class YourClass {
private String areacode;
private String phone3;
private String phone4;
}
For more an other idea have a look at the crossfield validation discussions/questions.
Cross field validation with Hibernate Validator (JSR 303)