Reputation: 303
An user upload his comment via this form.
Thymeleaf
<form th:action="@{/comment}" th:id="form" method="post">
<input type="hidden" th:name="productId.id" th:value="${product.id}">
<textarea th:field="${comment.message}" class="comment"
placeholder="Write comment here"></textarea>
<input type="submit" id="submit" value="comment">
</form>
Actual HTML
<form action="/comment" id="form" method="post" class="">
<input type="hidden" name="_csrf" value="f6b3f296-3284-4d2d-a2b2-0a9975f5e071">
<input type="hidden" name="productId.id" value="38">
<textarea class="comment" placeholder="Write comment here" id="message" name="message"></textarea>
<input type="submit" id="submit" value="comment">
</form>
However if user overwrites the actual HTML like this, the product's name will be changed to "ABCD"
<form action="/comment" id="form" method="post" class=""><input type="hidden" name="_csrf" value="f6b3f296-3284-4d2d-a2b2-0a9975f5e071">
<input type="hidden" name="productId" value="38">
<input type="hidden" name="productId.name" value="ABCD">
<textarea class="comment" placeholder="Write comment here" id="message" name="message"></textarea>
<input type="submit" id="submit" value="comment">
</form>
I think what happened here is Spring queried the productId
and it became managed Entity
, and when the user set the name to be "ABCD", it would be saved.
Here is my solution:
Basically just use @Validated
with a bunch of groups and put constraint with appropriate groups (UploadCommentValidation
in this case) on every single field, which works but seems really messy especially when it gets big.
Example with upload comment above:
productId
and message
must be @Not Null
, productId
must be @Valid
,other fields must be @Null
Id
must be @NotNull
, other fields must be @Null
Comment entity
public class Comment implements Comparable<Comment> {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Null(groups = {UploadCommentValidation.class})
@NotNull(groups = {DeleteCommentValidation.class, UpdateCommentValidation.class})
private Integer id;
@ManyToOne
@JoinColumn(name = "product_id", referencedColumnName = "id")
@JsonBackReference
@Valid
@NotNull(groups = {UploadCommentValidation.class})
@Null(groups = {DeleteCommentValidation.class, UpdateCommentValidation.class})
private Product productId;
@ManyToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
@JsonBackReference
@Null(groups = {UploadCommentValidation.class, DeleteCommentValidation.class, UpdateCommentValidation.class})
private User userId;
@Column(name = "message")
@NotBlank(message = "please write a comment", groups = {UploadCommentValidation.class, UpdateCommentValidation.class})
@Null(groups = {DeleteCommentValidation.class})
private String message;
@Column(name = "created_at", insertable = false, columnDefinition = "timestamp with time zone not null")
@Temporal(TemporalType.TIMESTAMP)
@Null(groups = {UploadCommentValidation.class, DeleteCommentValidation.class, UpdateCommentValidation.class})
private Calendar createdAt;
@Column(name = "updated_at", columnDefinition = "timestamp with time zone not null")
@Temporal(TemporalType.TIMESTAMP)
@Null(groups = {UploadCommentValidation.class, DeleteCommentValidation.class, UpdateCommentValidation.class})
private Calendar updatedAt;
@Override
public int compareTo(Comment o) {
return this.getId().compareTo(o.getId());
}
}
Product entity
public class Product implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@NotNull(message = "product id null", groups = {AddOrderValidation.class, UploadCommentValidation.class})
@Null(message = "bad request", groups = {ProductRegisterValidation.class})
private Integer id;
@NotBlank(message = "please fill in product name", groups = {ProductRegisterValidation.class})
@Length(max = 255, message = "too long", groups = {ProductRegisterValidation.class})
@Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
@Column(name = "name")
private String name;
@Column(name = "price")
@Positive(message = "the price must be non-negative", groups = {ProductRegisterValidation.class})
@NotNull(message = "please fill in price", groups = {ProductRegisterValidation.class})
@Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
private Integer price;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", referencedColumnName = "id")
@Valid
@NotNull(message = "please select category name", groups = {ProductRegisterValidation.class})
@Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
private Category categoryId;
@NotBlank(message = "please fill in description", groups = {ProductRegisterValidation.class})
@Length(max = 10000, message = "too long", groups = {ProductRegisterValidation.class})
@Null(groups = {AddOrderValidation.class, UploadCommentValidation.class})
@Column(name = "description")
private String description;
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
private List<ProductImage> productImages;
@OneToOne(mappedBy = "product", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
private Thumbnail thumbnail;
@OneToMany(mappedBy = "productId", fetch = FetchType.LAZY)
@JsonManagedReference
@Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
private List<Comment> comments;
@OneToMany(mappedBy = "product", fetch = FetchType.LAZY)
@Null(groups = {ProductRegisterValidation.class, AddOrderValidation.class, UploadCommentValidation.class})
private List<Order> orders;
}
Any ideas how to do it the right way? This seems super messy!
UPDATE 1: This is my rest controller
@PostMapping("/comment")
public ResponseEntity<Map<String, String>> commentResponseEntity(@Validated({UploadCommentValidation.class}) Comment comment, BindingResult result) {
if (result.hasErrors()) {
result.getAllErrors().forEach(System.out::println);
return ResponseEntity.noContent().build();
}
User user = getUser();
comment.setUserId(user);
commentRepository.saveAndFlush(comment);
Map<String, String> response = new HashMap<>();
response.put("comment", comment.getMessage());
response.put("user", user.getName());
response.put("commentId", comment.getId().toString());
return ResponseEntity.ok().body(response);
}
Upvotes: 0
Views: 113
Reputation: 23226
You can do this by registering an @InitBinder
method
You can do this at the individual controller level or by registering a @ControllerAdvice
to be applied to all, or a subset of all, controllers.
@InitBinder()
public void initBinder(WebDataBinder binder) {
binder.setDisallowedFields(new String[] { "id", "version" });
}
Upvotes: 1