thymeleaf mapping of object that has an inner object within a form - spring

i have 2 objects one is persisting (product) the other is an an embeddable address (storeWhereLocated), this object persists within product.
i am trying to create a form to persist the product using thymeleaf.
here is the Product entity im trying to persist
public class Product {
#Embedded
#AttributeOverrides(value = {
#AttributeOverride(name = "addressLine1", column = #Column(name = "address")),
#AttributeOverride(name = "addressLine2", column = #Column(name = "unit")),
#AttributeOverride(name = "city", column = #Column(name = "city")),
#AttributeOverride(name = "state", column = #Column(name = "state")),
#AttributeOverride(name = "zipCode", column = #Column(name = "zip_code"))
})
private Address storeWhereLocated;
}
this is the Address object no persisting needed here only within product entity
#Embeddable
public class Address {
#NotNull
#Size(min=2, max = 100)
private String addressLine1;
#NotNull
#Size(max = 50)
private String addressLine2;
#NotNull
#Size(min=2, max = 50)
private String city;
#NotNull
#Size(min= 2, max = 25)
private String state;
#NotNull
#Size(min=1, max = 6)
private String zipCode;
here is the form
<form th:action="#{/product/add}" method="post" th:object="${product}"
enctype="multipart/form-data">
<input th:field="*{storeWhereLocated.addressLine1}"/>
<input th:field="*{storeWhereLocated.addressLine2}"/>
<input th:field="*{storeWhereLocated.city}"/>
<input th:field="*{storeWhereLocated.state}"/>
<input th:field="*{storeWhereLocated.zipCode}"/>
</form>
my question is will this work the way i have it or not. i am trying to read up on it but could not come across anything.

Related

how to add object with fk to table in jparepository

i had scheme of user parking and detail parking.
user can park many times (one to many)
im trying to add detail parking object to my db, but i dont have idea how to add the fk from the user in the row of the table, its gave me null there.
(ignore from the logic of the model, i just want to understood the logic how can i the object with fk of ther entity)
this is my code:
#PostMapping("/parking")
public String saveCarParking(#ModelAttribute("user") parkingUsers parkingUsers) {
// parkingUsers[id, firstName, lastName, license]
parkingUsers p = new parkingUsers("jhon", "nash", "248651355");
parkingUsersService.saveParkingUser(p);
// parkingDetails[id, entryDate, entryTime, exitDate, exitTime, user_id(FK)]
parkingDetails d = new parkingDetails(LocalDate.now(), null, LocalDate.now(), null);
parkingDetailsService.saveParkingUser(d);
//how i connect parkingDetails object with fk of parkingUsers?
//it adding now row of parkingDetails but without the fk of user
return "redirect:/parkingList";
}
parking user entity:
#Entity
#Table(name ="users")
public class parkingUsers {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "first_name")
private String firstName;
#Column(name = "last_name")
private String lastName;
#Column(name = "license")
private String license;
#OneToMany(mappedBy = "parkingUsers", cascade = CascadeType.ALL, orphanRemoval = true)
private List<parkingDetails> parkingDetails = new ArrayList<parkingDetails>();
public parkingUsers() {
}
public parkingUsers(String firstName, String lastName, String license) {
this.firstName = firstName;
this.lastName = lastName;
this.license = license;
}
//setter gettrs and tostring...
entity class of details parking
#Entity
#Table(name ="details")
public class parkingDetails {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "entry_date")
private LocalDate entryDate;
#Column(name = "entry_time")
private LocalDateTime entryTime;
#Column(name = "exit_date")
private LocalDate exitDate;
#Column(name = "exit_time")
private LocalDateTime exitTime;
#ManyToOne
#JoinColumn(name="user_id")
private parkingUsers parkingUsers;
public parkingDetails() {}
public parkingDetails(LocalDate entryDate, LocalDateTime entryTime, LocalDate exitDate, LocalDateTime exitTime) {
this.entryDate = entryDate;
this.entryTime = entryTime;
this.exitDate = exitDate;
this.exitTime = exitTime;
}
//test
// public parkingDetails(LocalDate entryDate, LocalDateTime entryTime, LocalDate exitDate, LocalDateTime exitTime, int user_id ) {
// this.entryDate = entryDate;
// this.entryTime = entryTime;
// this.exitDate = exitDate;
// this.exitTime = exitTime;
// this.parkingUsers.setId(user_id);
// }
//setter gettrs and tostring...
In the ParkingDetails entity, you can have a setter for "parkingUsers" variable to set user object.
In your REST api's saveCarParking() method, before calling "parkingDetailsService.saveParkingUser(d);" you can pass the user object to ParkingDetails using setter created in ParkingDetails.
This should work. No need to explicitly extract the user_id from user's object to pass into ParkingDetails.
Adding one more parameter of type ‘parkingUsers‘ in the constructor of ‘ ParkingDetails’ to initialize user in parking class will also work.
(Apart, it is a good practice to start the class name with a capital letter e.g. instead of having class name as parkingDetails, it should be ParkingDetails.)

How to prevent user from injecting field into form backing bean?

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:
Comment Entity: productId and message must be #Not Null, productId must be #Valid,other fields must be #Null
Product Entity: 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);
}
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" });
}

How to insert values into PostConstruct

I have a H2 in-memory-database with 4 tables.
The table Shop has a connection to Address table.
Now I want to insert some values in my PostConstruct method.
So do my table definitions look like:
Shop:
#NotBlank
#Size(min = 2, max = 50)
#Column(name ="name")
private String name;
#NotNull
#ManyToOne
#JoinColumn(name ="address")
private Address address;
#ElementCollection
#JoinTable(name = "shop_products")
private List<Product> products;
Address
#NotBlank
#Size(min = 2, max = 50)
#Column(name ="address", unique = true)
private String address;
Product
#NotBlank
#Size(min = 2, max = 50)
#Column(name ="name", unique = true)
private String name;
#NotBlank
#Size(min = 2, max = 50)
#Column(name ="manufacturer")
private String manufacturer;
#NotNull
#Column(name ="currentPrice")
private BigDecimal currentPrice;
And these are my Service classes with the PostConsutrct method:
Address
#PostConstruct
public void initDb() {
Address address1 = Address.builder().address(AddressEnum.BURGENLAND.toString()).build();
Address address2 = Address.builder().address(AddressEnum.WIEN.toString()).build();
Address address3 = Address.builder().address(AddressEnum.TIROL.toString()).build();
addressRepository.saveAll(List.of(address1, address2, address3));
}
Product
#PostConstruct
public void initDb() {
Product product1 = Product.builder().name(ProductEnum.HAMMER.toString()).currentPrice(new BigDecimal(10)).manufacturer("Makita").build();
Product product2 = Product.builder().name(ProductEnum.KNIFE.toString()).currentPrice(new BigDecimal(20)).manufacturer("Bosch").build();
Product product3 = Product.builder().name(ProductEnum.NAIL.toString()).currentPrice(new BigDecimal(30)).manufacturer("Hilti").build();
productRepository.saveAll(List.of(product1, product2, product3));
}
Shop
#PostConstruct
public void initDb() {
Shop shop1 = Shop.builder().name("Shop 1").address(new Address(AddressEnum.BURGENLAND.toString())).build();
Shop shop2 = Shop.builder().name("Shop 2").address(new Address(AddressEnum.BURGENLAND.toString())).build();
Shop shop3 = Shop.builder().name("Shop 3").address(new Address(AddressEnum.BURGENLAND.toString())).build();
shopRepository.saveAll(List.of(shop1, shop2, shop3));
}
If I delete the PostConstruct in my ShopService class, it works, but then I do not have any data in my Shop table.
Whereas running my application with the PostConstruct, I always get the following error message:
TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation
The problem is I do not know how to get rid of the error message?
What I did change now was the following:
I added cascade = CascadeType.ALL in my Shop model class.
#NotNull
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name ="address")
private Address address;
Now, it works.

No values in dropdown lists with Thymeleaf and SpringBoot

I have a similar issue on my drop down list not showing any values. Like
the solution: dropdown lists with Thymeleaf and SpringBoot No data showing
in dropdown list. If I look in the IE debugger I can see my list for the
dropdown, but the dropdown won't display my list to pick from.
The HTML snippet is here:
<div class="col-lg-3 selectContainer">
<div class="input-group">
<span class="input-group-addon"><i
class="glyphicon glyphicon-list"></i></span>
<select th:field="*{savings_type}" name="savings_type_id"
id="savings_type_id" class="form-control selectpicker">
<option value="">Select Savings Type</option>
<option th:each="dropDownItem : ${leanTypesList}"
th:value="${dropDownItem.lt_id}"
th:text="${dropDownItem.lt_name}">
</option>
</select>
</div>
</div>
Code here:
My Model leanDeatil.java - data populating Savings_Type field
This is value is going too.
#Entity
#Table(name="lean_detail")
public class LeanDetail implements Serializable {
private static final long serialVersionUID = 1L;
#Id
//#Column (name="LEANDETAIL_ID")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#Column (name="SAVINGS_TYPE")
private int savings_type;
#Column (name="SAVING_AMOUNT")
private BigDecimal saving_amount;
#Column (name="TITLE")
private String title;
#Column (nullable=true, name="IMPROVEMENT_DESC")
private String improvement_desc;
#ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinColumn(name = "SAVINGS_TYPE", referencedColumnName = "lt_id",
insertable = false, updatable = false)
private LeanTypes leanTypes;
public LeanDetail(){}
public LeanDetail(String title,int savings_type, BigDecimal saving_amount,
String improvement_desc {
super();
this.title = title;
this.savings_type = savings_type;
this.saving_amount = saving_amount;
this.improvement_desc = improvement_desc;
}
getters and setters....
My Model leanTypes.java data coming from
#Entity
#Table(name="lean_types")
public class LeanTypes {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
#Column (name="LT_ID")
private int lt_id;
#Column (name="LT_NAME")
private String lt_name;
#Column (name="LT_DESC")
private String lt_desc;
#Column(nullable=false, name="LT_ACTV")
private boolean lt_actv;
#OneToMany(mappedBy="leanTypes", fetch=FetchType.EAGER)
#Fetch(value = FetchMode.SUBSELECT)
private List<LeanDetail> leanDetail = new ArrayList<LeanDetail>();
public LeanTypes(){}
public LeanTypes(int lt_id, String lt_name, String lt_desc, Boolean lt_actv) {
super();
this.lt_id = lt_id;
this.lt_name = lt_name;
this.lt_desc = lt_desc;
this.lt_actv = lt_actv;
}
getters and setters....
}
From my Controller LeanDetail.java
#GetMapping("/update-leanDetail")
public String updateLeanDetail(#RequestParam int id, HttpServletRequest
request){
request.setAttribute("leanTypesList", leanTypesService.findAll());
request.setAttribute("leanDetail", leanDetailService.findLeanDetail
(id));
request.setAttribute("mode", "MODE_UPDATE");
return "leanDetail";
}

Spring 4 MVC Form: Create Object with list of sub objects

I looking for help in understanding how to create a new object through a form which has the user select multiple sub-objects (which will come pre-populated) and available to select with a checkbox.
OrderController.java
#RequestMapping(value = { "/order" }, method = RequestMethod.GET)
public String order(ModelMap model) {
List<Exam> exams = examService.findAllExams();
List<Document> documents = documentService.findAllDocuments();
model.addAttribute("exams", exams);
model.addAttribute("documents", documents);
return "order"; // jsp page reference
}
Order.java
#Entity
#Table(name="\"order\"")
public class Order implements Serializable
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "order_id", unique = true, nullable = false)
private Integer id;
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
#Column(name = "uuid", unique = true, nullable = false)
private String uuid;
#Temporal(TemporalType.DATE)
#Column(name = "order_date", unique = true, nullable = false)
private Date orderDate;
#Column(name="order_status", nullable=false)
private String orderStatus;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "user_id", nullable = false)
private User user;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "order")
private Set<OrderExam> orderExams = new HashSet<OrderExam>(0);
#OneToMany(fetch = FetchType.LAZY, mappedBy = "order")
private Set<OrderDocument> orderDocuments = new HashSet<OrderDocument(0);
//getters & setters
}
OrderExam.java
#Entity
#Table(name="order_exam")
public class OrderExam implements Serializable
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "order_exam_id", unique = true, nullable = false)
private Integer id;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "order_id", nullable = false)
private Order order;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "exam_id", nullable = false)
private Exam exam;
#Column(name="exam_amount", nullable=true)
private Integer examAmount;
#Column(name="answer_sheet_amount", nullable=true)
private String answerSheetName;
#Column(name="students_per_csv", nullable=true)
private String studentsPerCSV;
#Column(name="pas", nullable=true)
private Boolean pearsonAnswerSheet;
//getters & setters
}
Exam.java
#Entity
#Table(name="exam")
public class Exam implements Serializable
{
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name = "exam_id", unique = true, nullable = false)
private Integer id;
#NotEmpty
#Column(name="name", unique=true, nullable=false)
private String name;
#NotEmpty
#Column(name="code", unique=true, nullable=false)
private String code;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "exam")
private Set<OrderExam> exams = new HashSet<OrderExam>(0);
//getters & setters
}
As you can see I am passing in a list of exams and documents which will populate a form with available options (can be seen in the image below (exams anyway)). The user needs to be able to select multiple rows, so that a single order has multiple exams and documents associated to it.
My order.jsp is a little much to post the entire thing here but here is the part I have which is displayed in the image above.
Order.jsp
<form:form method="POST" modelAttribute="order" class="form-horizontal form-label-left">
<c:forEach items="${exams}" var="exam">
<tr>
<th scope="row"><input type="checkbox" class="flat"></th>
<td><input id="middle-name" type="text" name="middle-name" readonly="readonly" value="${exam.name} - ${exam.code}" class="form-control col-md-7 col-xs-12"></td>
<td><input id="middle-name" type="text" name="middle-name" value="0" class="form-control col-md-3 col-xs-12"></td>
<td><input id="middle-name" type="text" name="middle-name" value="0" class="form-control col-md-3 col-xs-12"></td>
<td><input id="middle-name" type="text" name="middle-name" value="0" class="form-control col-md-3 col-xs-12"></td>
<c:choose>
<c:when test="${exam.name == 'Algebra 2 (Common Core)'}">
<th scope="row"><input type="checkbox" class="flat"></th>
</c:when>
<c:otherwise>
<th scope="row"></th>
</c:otherwise>
</c:choose>
</tr>
</c:forEach>
<!-- Other Stuff Goes Here -->
</form:form>
So in short, would someone be willing to show me how to set up the form in the way I described above? Thanks in advance.
Your question is a bit broad however you could try this as below. I have only covered exams. Principal for documents will be the same.
You will need a couple of new classes to capture the submitted form inputs:
Order Form to Capture Selections
public class OrderForm{
private List<ExamWrapper> allAvailableExams = new ArrayList<>();
private XOptionPrintWrapper selectedWrapper;
public OrderForm(){
}
//getters and setters
}
Exam Wrapper : Decorates an Exam with a 'selected' property
public class ExamWrapper{
private boolean selected;
private Exam exam;
public ExamWrapper(Exam exam){
this.exam = exams;
}
//getters and setters
}
Change Contoller to
public class OrderController{
//Exams model populated by the method below
//moved as we also need it populated on POST
#RequestMapping(value = { "/order" }, method = RequestMethod.GET)
public String order(ModelMap modelMap) {
//only needed on GET so put in model here
List<XOptionPrintWrapper> availableWrappers = //someList;
modelMap.put("availableWrappers", availableWrappers);
return "order";
}
//handles for submit
//model atribute is automatically populated by the framework
#RequestMapping(value = { "/order" }, method = RequestMethod.POST)
public String order(#ModelAttribute("orderForm") OrderForm orderForm) {
//process selected exams
return "nextView";
}
//on get populates the initial model for display
//on post create an instance which the form params will be bound to
#ModelAttribute("orderForm")
public OrderForm getOrderForm(){
OrderForm orderForm = new OrderForm();
List<Exam> exams = examService.findAllExams();
for(Exam exam : exams){
orderForm.getAllAvailableExams.add(new ExamWrapper(exam));
}
return orderForm;
}
}
In JSP use Sping support for binding to indexed properties:
<form:form method="POST" modelAttribute="orderForm" class="form-horizontal form-label-left">
<c:forEach items="${orderForm.allAvailableExams}" var="exam" varStatus="status">
<tr>
<th scope="row"><input name="allAvailableExams[${status.index}].selected"
type="checkbox" class="flat"></th>
</tr>
</c:forEach>
<form:select path="selectedWrapper">
<form:options items="${availableWrappers}"
itemValue="somePropertyOfXOptionPrintWrapper "
itemLabel="somePropertyOfXOptionPrintWrapper " />
</form:select>
</form>
I obviously haven't been able to try all of this but think it should all be okay.

Resources