I am completely stuck on mapping a controller to a URL. I have searched quite a bit, but cannot figure out what I'm doing wrong, except from the fact that the mapping is just not happening. The parts included are currently the only ones not working.
Controller Code (Part of a bigger controller file with another controller method with value "/course_users")
#RequestMapping(value = "/new_users", method = RequestMethod.GET)
public String addStudent(
Model model,
Principal principal,
HttpServletRequest requestID, HttpServletRequest requestName)
{
Long courseId = Long.parseLong(requestID.getParameter("id"));
User currentUser = userService.findByEmail(principal.getName());
Course currentCourse = courseService.findCourseById(courseId);
model.addAttribute("user", currentUser);
model.addAttribute("courseId", courseId);
try{
if(!currentUser.getRoles().contains(Role.ADMIN) && !currentUser.getRoles().contains(Role.LECTURER)){
String errorMsg = "Nie masz wystarczających uprawnień, aby dodać kursanta";
model.addAttribute("errorMsg", errorMsg);
return "error";
}
List<User> users = userService.findAll();
model.addAttribute("users", users);
String userName = requestName.getParameter("userName");
User newStudent = userService.findByEmail(userName);
courseService.addStudentToCourse(currentCourse, newStudent);
}
catch (IllegalArgumentException e){
String errorMsg = "Podano nieprawidłowe argumenty podczas tworzenia kursu.";
model.addAttribute("errorMsg", errorMsg);
return "error";
}
return "course_users";
}
HTML file code (the file is called "course_users.html")
<div class="container pb-3">
<form th:action="#{/new_users}">
<div class="form-group">
<label for="userName">Dodawany Student:</label>
<select class="form-control" id="userName" th:name="userName">
<option th:each="student : ${users}" th:value="${student.getEmail()}" th:text="${student.getEmail()}"></option>
</select>
</div>
<div class="pt-3">
<button type="submit" class="btn btn-primary">Dodaj studenta</button>
</div>
</form>
EDIT
Relevant part of UserService (Course Service is annotated the same way)
#Service
public class UserService {
private final UserRepository userRepository;
#Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
//rest of service code
}
The error is located in the header, and is attempting to call getFirstName() on user, which it doesn't seem to be getting (and why I'm assuming the controller is not getting mapped).
I had an identical problem (and error) with the first method, where I swapped #PathVriable for HttpServletRequest which fixed the problem, sadly no such luck here.
Method call: Attempted to call method getFirstName() on null context object
I feel like this is most likely caused by something very minor that I just keep missing.
EDIT2
I decided to see what happens if I (once again) try
#RequestParam(value = "userName") String userName and #PathVariable("courseId") Long courseId with value = "/new_users/{courseId}"
That went about the same until I swapped
<option th:each="student : ${users}" th:value="${student.getEmail()}" th:text="${student.getEmail()}"></option>
with
<option th:each="student : ${course.getEnrolledStudents()}" th:value="${student.getEmail()}" th:text="${student.getEmail()}"></option>
which showed me an entry in the selection list and gave me an expected error page on clicking the button! (since I was trying to add the same person a second time)
Could be that I'm just mucking something up in HTML syntax, or using/adding the model variable users wrong?
Your controller looks fine can you check you annotated your courseService and userService with #Autowired annotation or not.
#Autowired
private CourseService courseService
Share your error message if it doesn't solve your problem.
I understand that this way you would access the user's values added to model, having always created the corresponding getters and setters of each attribute:
<select class="form-control" id="userName" th:name="userName">
<option th:each="student : ${users}" th:value="${student.email}"
th:text="${student.email}"></option>
</select>
Finally found and answer!
Instead of trying to add a model attribute through model.addAttribute, just add it like this:
#ModelAttribute("users")
public List<User> users() {
return userService.findAll();
}
I still couldn't tell you why the other method didn't work, but at least here's one that works.
Related
I'm rather new to Thymeleaf, so this maybe a newbie mistake. And I've been looking everywhere online for an answer and haven't found one. So sorry if this is really basic.
Basically I'm using Thymeleaf with SpringBoot 2.6.7 and I want to populate a object using a form. Something I've been able to do in the past, but this time for the first time the object I want to populate contains a list of other objects. And it's getting quite tricky.
My html looks like this :
<form action="#" th:action="#{/character}" th:object="${input}" method="post">
<div th:each="i : ${#numbers.sequence(0, input.attributes.size - 1)}">
<div th:object="${input.attributes[i]}">
<input type="range" min="1" th:max="*{maxValue}" th:field="*{value}"> <!-- this doesn't work -->
<p th:text="*{name} + ' = ' + *{value}"></p> <!-- this works -->
</div>
</div>
</form>
The error I get is java.lang.NumberFormatException: For input string: "i". So I'm guessing there's an issue with the parsing of attributes[i] when processing th:field.
I've tried to change the loop to <div th:each="attribute : ${input.attributes}"> (and the associated th:object) but that just made it worse, got the Neither BindingResult nor plain target object for bean name 'attribute' available as request attribute error message.
If it's any help, here is my controller :
#GetMapping("character")
public String startingForm(Model model) {
model.addAttribute("input", new FormInput());
return "character";
}
#PostMapping("character")
public String processingForm(#ModelAttribute FormInput input, BindingResult bindingResult, Model model)
throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException, IOException {
if(bindingResult.hasErrors()){
log.error("something went wrong");
}
// Other stuff
return "character";
}
My input class
#Getter
public class FormInput{
protected List<Attribute> attributes;
public FormInput(){
attributes= new ArrayList<>();
for (AttributeEnum ae : AttributeEnum.values()) {
attributes.add(new Attribute(ae.getName(), ae));
}
}
}
And the Attribute class
#Getter
public class Attribute {
protected String name;
protected AttributeEnum typeAttribute;
protected Integer value = 1;
protected Integer maxValue = 5;
public Attribute(String n, AttributeEnum ta){
name = n;
typeAttribute = ta;
}
public boolean setValue(Integer v){
if (v > maxValue ) return false;
value = v;
return true;
}
}
Does someone know a fix to this issue?
Just found the solution. So I'm posting the answer is anyone has the same issue.
<div th:each="i : ${#numbers.sequence(0, input.attributes.size - 1)}">
<input type="range" min="0" th:max="${input.attributes[i].maxValue}" th:field="${input.attributes[__${i}__].value}"> <!-- this works -->
</div>
Just had to replace attributes[i] with attributes[__${i}__].
I'm developing Spring + Thymeleaf application. I'm implementing search with multiple params. I have a form with the corresponding DTO. Here is the code of the DTO:
public class ClassSearchDto {
private String searchParam;
private Long programId;
private List<String> teacherNames;
//getters, setters and constructor are omitted
}
As you see, I have a list of strings in my DTO called teacherNames. Here is the way I'm displaying my form:
<form th:action="#{/classes/search}" method="get" th:object="${classSearchDto}">
<div class="form-group">
<input type="hidden" class="form-control"
th:value="${classSearchDto.programId}" th:field="*{programId}"/>
<label for="searchParam">Search</label>
<input type="text" class="form-control" id="searchParam" placeholder="keyword"
th:value="${classSearchDto.searchParam}" th:field="*{searchParam}"/>
<div>
<th:block th:each="name, iter ${classSearchDto.teacherNames}">
<input th:value="${name}" th:field="*{teacherNames[__${iter.index}__]}/>
</th:block>
</div>
</div>
<button class="btn btn-default" type="submit">Find</button>
</form>
I want to implement my search with help of #RequestParam annotation on the back-end. This is my controller:
#RequestMapping(value = "/search")
public String findClassByName(#RequestParam("searchParam") final String searchParam,
#RequestParam("programId") final Long programId,
#RequestParam("teacherNames") final List<String> teacherNames,
final Model model) {
...
}
The problem is that I can't get the list of teacher names in this way. I get this exception:
org.springframework.web.bind.MissingServletRequestParameterException:Required List parameter 'teacherNames' is not present
Could you please help me to transfer the list of elements in DTO to my back-end with this approach? Maybe you know how to do it correctly in another way. Thank you in advance.
I can suggest you one thing, I don't know whether it works or not. Try changing
public String findClassByName(#RequestParam("searchParam") final String searchParam,#RequestParam("programId") final Long programId,#RequestParam("teacherNames") final List<String> teacherNames,final Model model)
to
public String findClassByName(#ModelAttribute("classSearchDto") ClassSearchDto classSearchDto,#RequestParam("searchParam") String searchParam,#RequestParam("programId") Long programId,#RequestParam("teacherNames") List<String> teacherNames,Model model)
I'm skilling in form validation with spring boot and thymeleaf and i have problem: i can't do validation in form with two #ModelAttribute fields. The example like form validation om spring official site works correctly, but when I added two #model Attribute in post i get only error at webpage and no hints at form like in spring example.
Controller class:
#Controller
public class MyController {
#Autowired
InstructorRepository instructorRepository;
#Autowired
DetailRepository detailRepository;
#GetMapping("/index")
public String mainController(){
return "index";
}
#GetMapping("/add")
public String addInstructorForm(Model model){
model.addAttribute("instructor", new Instructor());
model.addAttribute("detail", new InstructorDetail());
return "addInstructor";
}
#PostMapping("/add")
public String submitForm(#Valid #ModelAttribute Instructor instructor, #ModelAttribute InstructorDetail instructorDetail, BindingResult bindingResult1){
/* if (bindingResult.hasErrors()) {
return "instructorsList";
}
instructor.setInstructorDetail(instructorDetail);
instructorRepository.save(instructor);*/
if (bindingResult1.hasErrors()) {
return "addInstructor";
}
return "redirect:/instructorsList";
}
#GetMapping("/instructorsList")
public String getList(Model model){
Map map = new HashMap<>();
List list = new ArrayList<Instructor>();
list = instructorRepository.findAll();
List resultList = new ArrayList();
for (int i = 0; i < list.size(); i++) {
Instructor instructor = (Instructor)list.get(i);
InstructorDetail detail = detailRepository.getInstructorDetailById(instructor.getId());
InstructorAndDetail iid = new InstructorAndDetail(instructor, detail);
resultList.add(iid);
}
model.addAttribute("instructors", resultList);
return "instructorsList";
}
}
html form snippet:
<form action="#" data-th-action="#{/add}" data-th-object="${instructor}" method="post">
<div class="form-group">
<label for="1">First name</label>
<input class="form-control" id="1" type="text" data-th-field="${instructor.firstName}" placeholder="John"/>
<div data-th-if="${#fields.hasErrors('firstName')}" data-th-errors="${instructor.firstName}">name error</div>
</div>
There was next problem: then I add entity to thymeleaf form I passed only 2 fields (or one field after), but there was 3 fields with
#NotNull
#Size(min=2, max=30)
So when I commented them in my code the single field validation begin to work :).
If you stucked at the same problem check that all you fields in class that marked #Valid annotation are mirrored in your form.
(or have default valid values? UPD: dont work with valid defaults if they have no form mirroring)
Validating the overlapping of date intervals, in the implementation of the Validator class. I get a list from a Posgresql database, which I would like to show, next to the error message.
I've tried to insert it, without succeeding, with this line of code:
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
That's the complete code:
#Controller
public class PricesController {
#Autowired
private RateRepository rateRepository;
#Autowired
private PricesValidator pricesValidator;
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(pricesValidator);
}
//Enter new price form
#GetMapping("/admin/rates/priceform")
public String priceForm(Model model){
model.addAttribute("rate", new Rate());
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
return "/admin/rates/priceform";
}
#PostMapping("/admin/rates/priceform")
public String priceSubmit(#ModelAttribute #Valid Rate price, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "/admin/rates/priceform";
}
rateRepository.addRate(price);
return "redirect:/admin/rates/prices";
}
}
I use Thymeleaf, but I take for granted that the problem is not with the viewer.
This is the html view:
<!--Global validation results-->
<div th:if="${#fields.hasErrors('global')}">
<div class="alert alert-danger" role="alert"
th:each="err : ${#fields.errors('global')}">
<div th:switch="${err}">
<p th:case="error.fromAfterTo" th:text="#error.fromAfterTo}"></p>
<p th:case="error.overlaps" th:text="#{error.overlaps}"></p>
<ul>
<li th:text="#{from} + ' - ' + #{to}"></li>
<li th:each="interval : ${dateOverlaps}"
th:text="${#temporals.format(interval.datefrom, 'dd/MM/yyyy')} + '-' +
${#temporals.format(interval.dateto, 'dd/MM/yyyy')}">Intervals</li>
</ul>
</div>
</div>
</div><!--Global validation results-->
Thank you in advance for your help.
The problem is that pricesValidator.getDBIntervals() at #GetMapping:
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
is calling the list that I want to show with the message error, which is empty until the form has been submitted. Because its values, are picked up from the database, with the validation process.
That raises a new question:
How can I set this model attribute in the #PostMapping section?
The answer to this question is simple, in the #PostMapping section, you can set a Model, as well as in the #GetMapping one. In my case, this was the solution:
#PostMapping("/admin/rates/priceform")
public String priceSubmit(#ModelAttribute #Valid Rate price, BindingResult bindingResult, Model model){
if(bindingResult.hasErrors()){
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
return "/admin/rates/priceform";
}
rateRepository.addRate(price);
return "redirect:/admin/rates/prices";
}
In my current spring project, I have a generic controller like this:
public class basicController<E> {
#Autowired
private basicService<E> serv;
protected Class<?> clazz;
public basicController(Class<?> clazz) {
this.clazz = clazz;
}
...
}
with a method like this:
#ModelAttribute("lista")
public List<E> populateList() {
return serv.lista();
}
I wonder if it's possible use the value for lista in a structure like that (in the html page):
<select class="form-control" th:name="...">
<option th:each="opt : ${lista}" th:value="${opt.getId()}"><span th:text="${opt}"/>
</option>
</select>
this page is mapped in the controllers with methods like that:
generic controller
#RequestMapping(value = "cadastra")
#PreAuthorize("hasPermission(#user, 'cadastra_'+#this.this.name)")
#Menu(label = "cadastra")
public String cadastra(Model model) throws Exception {
model.addAttribute("command", serv.newObject());
return "private/cadastra";
}
home controller (contains mappings for public views, among others things)
#RequestMapping(value = "/settings")
public String settings(Model model) throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("usuario", auth.getName());
model.addAttribute("menu", MenuList.index());
model.addAttribute("settings", MenuList.settings());
return "private/settings";
}
#RequestMapping(value = "/profile")
public String profile(Model model) throws Exception {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
model.addAttribute("usuario", auth.getName());
model.addAttribute("menu", MenuList.index());
model.addAttribute("command", usuario(auth.getName()));
return "private/profile";
}
Anyone have any idea about this?
Ok, I just test and verify no extra configuration is needed for use the value from a ModelAttribute method. So, I just add methods like this in my controller:
#ModelAttribute("lista")
public List<E> populateListPagina() {
return serv.lista();
}
#ModelAttribute("classe")
public String getName() {
return clazz.getSimpleName();
}
and when I access any mapped view, I can use the value returned by this method in the way I like:
<tbody class="content">
<tr th:each="item : ${lista}">
<th></th>
<th th:each="coluna : ${campos}" th:text="${item[coluna]}"></th>
<th>
<div class="btn-group" role="group" aria-label="menu_item">
<button th:each="btn : ${menu_item}" type="button" class="btn btn-default link" th:attr="data-href=#{/__${classe}__/__${btn}__/__${item.getId()}__}" th:text="${btn}"></button>
</div>
</th>
</tr>
</tbody>
#ModelAttribute fires up before your controller methods do, and will disappear once your method runs I believe. So you won't have the object in the view anymore, it acts more like a #RequestParam.
However, you can try adding #SessionAttributes("lista") if you're using newer version of Spring ( I believe 4+). You have to be careful to make sure you close the session attributes though. To close, do what this guy did - link.