I am using Spring Boot and Thymeleaf to create a single landing page for
my application. For this, I need to render a List of Host objects that all
contain a Container.
Here is the relevant code:
public class Container {
private String name;
private String baseUrl;
private String status;
public Container(String name, String baseUrl, String status) {
this.name = name;
this.baseUrl = baseUrl;
this.status = status;
}
public String getName() { return name; }
public String getBaseUrl() { return baseUrl; }
public String getStatus() { return status; }
}
public class Host {
private HashMap<String, Container> containers;
....
public List<Container> getContainers() {
return containers.values();
}
}
#RequestMapping("/")
public class IndexController {
#RequestMapping("/")
public String getIndex(Model model) {
model.addAttribute("hosts", hostRepository.getAllServers());
return "index";
}
}
Now I want to iterate over all servers and display the information about each Container in a table.
My Thymeleaf template looks like this:
<div class="panel panel-default" th:each="host : ${hosts}">
<div class="panel-heading">
<b th:text="${host.name}">Host X</b>
<div class="panel-body">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>URL</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr th:each="container : ${host.getContainers()}">
<!-- HERE IS THE PROBLEM -->
<td th:text="${container.name}">Service1</td>
<td th:text="${container.baseUrl}">domain.com/api/url</td>
<td th:text="${container.status}">RUNNING</td>
<!-- HERE ENDS THE PROBLEM -->
</tr>
</tbody>
</table>
</div>
</div>
</div>
My problem is the part where is access the container's properties (marked by the commentary).
Every time I get a SpringEL Exception. If I remove the th:text="${container.xy}" and replaces it with th:text="${container} a String version of the container is shown so I have access to the object and the loop it working properly. I also tried to replace the field access with getters (e.g. getStatus()) but it also does not work.
Thanks for your help. If you need more information, feel free to ask.
Setup:
Java 8
Spring Boot Starter Web
Thymeleaf
edit: The exception thrown is: nested exception is org.thymeleaf.exceptions.TemplateProcessingException: Exception evaluating SpringEL expression: "container.name" (index:35) where index:35 is the first problematic line.
The toString() output when using ${container} is jenkins=com.my.app.Container#7552c269 and jenkins is the name attribute of the Container instance.
Solution It seemed that the nested loop was iterating over a Map instead of a List. Changing ${container.xy} to ${container.getValue().xy} solved the problem.
Solution
It seemes that the nested loop was iterating over a org.thymeleaf.util.EvaluationUtil$MapEntry instead of a List. Changing ${container.xy} to ${container.getValue().xy} solved the problem.
Bits learned along the way:
Override the toString() method to obtain formatted information about the object iterating over. In this case the output was key=value which altough value was expected. This gave a hint that the current object must be something else than a Container instance
Look at the stack trace of Thymeleaf (usually its a hint that something is null or not public)
Use getClass() on the current object during the iteration to check if something went wrong here
Related
im using thymeleaf to pass index.html to model attribute containing LiveDataSet Class. but i keep encountering this error.
***org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'LiveDataSet' cannot be found on object of type 'com.ex.excom.controller.LiveDataController$LiveDataSet' - maybe not public or not valid?***
at org.springframework.expression.spel.ast.PropertyOrFieldReference.readProperty(PropertyOrFieldReference.java:217)
PropertyOrFieldReference.java:217
at org.springframework.expression.spel.ast.PropertyOrFieldReference.getValueInternal(PropertyOrFieldReference.java:104)
PropertyOrFieldReference.java:104
at org.springframework.expression.spel.ast.PropertyOrFieldReference.access$000(PropertyOrFieldReference.java:51)
PropertyOrFieldReference.java:51
at org.springframework.expression.spel.ast.PropertyOrFieldReference$AccessorLValue.getValue(PropertyOrFieldReference.java:406)
PropertyOrFieldReference.java:406
at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:90)
CompoundExpression.java:90
at org.springframework.expression.spel.ast.SpelNodeImpl.getValue(SpelNodeImpl.java:109)
SpelNodeImpl.java:109
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:328)
SpelExpression.java:328
at org.thymeleaf.spring5.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:263)
SPELVariableExpressionEvaluator.java:263
at org.thymeleaf.standard.expression.VariableExpression.executeVariableExpression(VariableExpression.java:166)
VariableExpression.java:166
at org.thymeleaf.standard.expression.SimpleExpression.executeSimple(SimpleExpression.java:66)
SimpleExpression.java:66
LiveDataController
#RestController
#Controller
public class LiveDataController extends BaseController
{
public class LiveDataSet
{
String getActive;
String getApparent;
String getCurrent;
String getEnergy;
}
public LiveDataSet getLiveDatas(){
...
LiveDataSet liveDatas = new LiveDataSet();
liveDatas.getCurrent = "1234"
liveDatas.getEnergy = "92.1"
...
return liveDatas
}
}
IndexController
#RequestMapping("/")
#PreAuthorize("hasAnyAuthority('ROLE_USER','ROLE_ADMIN')")
public String index(Model model, HttpSession session)
{
LiveDataSet liveData = liveDataController.getLiveDatas();
Optional<LiveDataSet> listOptLiveData = Optional.of(liveData);
listOptLiveData.ifPresent(LiveDataSet -> model.addAttribute("liveData", liveData));
return "index";
}
index.html
<tr th:each="row : ${liveData}">
<td th:text="${row.LiveDataSet}"></td>
</tr>
how i can handle this error.. data does exist, but i can even see the data in the view.
Thank you for reading this.
liveData that you are sending to view is an instance of LiveDataSet. In the view you are accessing it as ${row.LiveDataSet}. It doesn't make sense. There isn't any LiveDataSet field in your LiveDataSet! That's why you are getting this error.
You should access the attrs of the Object row (that is an instance of that LiveDataSet i belive)
<thead>
<tr>
<td>Col1</td>
<td>Col2</td>
<td>Col3</td>
</tr>
</thead>
<tbody>
<tr th:each="row : ${liveData}">
<td th:text="${row.attr1}"></td>
<td th:text="${row.attr2}"></td>
<td th:text="${row.attr3}"></td>
</tr>
</tbdoy>
[[${liveData}]]
public class LiveDataSet
{
String getActive; -> dActive;
String getApparent; -> dApparent;
String getCurrent; -> dCurrent;
String getEnergy; -> dEnergy
}
the problem is, variable naming. if i declare variable get + Active , it returns get, property so this cause java variable detect malfunction so i changed variable names and it safely returns value.
I encountered a similar issue and in the long run i realised i didn't have a getter/setter for them in their parent class (another downside of not using lombok). After the getter setter and renaming, it worked. Perhaps you should look into that.
I have a Controller like this:
#Controller
public class DeviceController {
#Inject
private DeviceService deviceService;
#ModelAttribute("devices")
public List<Device> getDevices() {
return deviceService.getAll();
}
#GetMapping({"/", "index.html"})
public String showIndex() {
return "index";
}
#DeleteMapping(value = "/devices/{id}")
public String deleteOne(#PathVariable("id") long id) {
deviceService.deleteOne(id);
return "index :: devices";
}
}
And a Thymeleaf template like this:
<table id="tbl_device" th:fragment="devices">
<tr> <!-- header --> </tr>
<tr th:each="e : ${devices}" th:object="${d}" th:id="'device_' + *{id}" th:fragment="'device_' + *{id}">
<!-- columns -->
</tr>
</table>
When I call the /devices/{id} DELETE endpoint, I would expect it to return the table without the deleted device. But it actually returns the table including the deleted device. When I debug the code, I can see that getDevices() is called before deleteOne(id).
When I manually reload the page after deleting, the row is (correctly) not displayed anymore.
Why is that? And (how) can I change this behaviour?
Thanks
Why is that?
I recommend to read this article. According to that:
In general, Spring-MVC will always make a call first to that method,
before it calls any request handler methods. That is, #ModelAttribute
methods are invoked before the controller methods annotated with
#RequestMapping are invoked. The logic behind the sequence is that,
the model object has to be created before any processing starts inside
the controller methods.
I doubt you can alter invocation order, but what you can do is additionally pass model attribute to your deleteOne method and modify it there.
#DeleteMapping(value = "/devices/{id}")
public String deleteOne(#PathVariable("id") long id, #ModelAttribute("devices") List<Device> devices) {
deviceService.deleteOne(id);
devices.remove( /* just deleted device */);
return "index :: devices";
}
It may be a silly qustion, but I still cannot find an answer to it.
My Spring Boot application looks something like this:
Model:
public class Company {
public static final String URL_COMPANY = "http://193.142.112.220:8337/companyList";
private Long iD;
private String companyName;
public static Map<Long, Object> companyMap;
public Long getiD() {
return iD;
}
public void setiD(Long iD) {
this.iD = iD;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
#Override
public String toString() {
return companyName;
}}
Controller:
#Controller
public class UrlController {
#GetMapping("/success")
public String show(Model model) {
HashMap<Long, Object> company = (HashMap<Long, Object>) Company.companyMap;
model.addAttribute("companyID", company);
return "success";
}
View:
<h1>All Companies:</h1>
<table border="1">
<tr>
<th>ID</th>
<th>Name</th>
</tr>
<tr th:each="mapEntry: ${companyID}">
<td th:text="${mapEntry.key}"></td>
<td th:text="${mapEntry.value}"></td>
</tr>
</table>
<a th:href="#{/}">Homepage</a>
</body>
</html>
So my goal is to display a table filled with Company ID's and Names. Even though, my model gets a map, I still can't see it in my browser. The table is empty.
</tr>
<tr th:each="mapEntry: {1=Tire Systems, 2=IT Enterprise, 3=Car Manufacture, 4=Electro Market}">
<td th:text=""></td>
<td th:text=""></td>
</tr>
This is what i get if i check the page source. So I clearly see, that map is loaded, but not displayed.
Moreover, a link with "homepage" does not work, and i am not sure why?
What am I missing? I am trying to fill a table with Companies, and then, using Id's of those companies, show materials attached to company via this ID. Can i use hyperlinks in table for Id's?
So you want to display a map. IF value of your map is a POJO Try something like followings
<tr><th>ID</th><th>Name</th></tr>
<tr th:each="mapEntry : ${companyID}">
<td th:text="${mapEntry.key}">keyvalue</td>
<td th:each="item : ${mapEntry.value}" th:text="${item.FIELD_NAME_OF_YOUR_POJO}">keyvalue</td>
</tr>
This should work. What i tried to show is, it is possible. The iteration depends on your data structure. If you have complex data structure iteration will change accordingly.
If value of your map is a primitive type or java reference type your current code should work. I have executed similar code like your's and it worked without any trouble. Please have a look -
HashMap<Long, Object> company = new HashMap<>();
company.put(1L, "Hello World");
model.addAttribute("companyID", company);
If value of your map is custom java type. Then follow the previous snippet.
I am very new with SPRING MVC so really I dont know much about it as of the moment. I want to display all the fields in the database in a table view how do I do this?
in my controller
#RequestMapping(value = "task", method = RequestMethod.GET)
public String taskList(Map<String, Object> model) {
model.put("task", taskRepository.findAll());
return "/tasks/list";
}
my jsp:
<%#include file="/WEB-INF/views/includes/header.jsp"%>
<h4 class="form-header">${title}</h4>
<div class="forms col-md-12 bounceInDown mainContent" data-wow-delay="0.2s">
<table class="table table-striped">
<thead>
<tr>
<th>Task ID</th>
<th>Task Name</th>
<th>Task Description</th>
</tr>
</thead>
<tbody>
<c:if test="${empty task}">
<tr>
<td colspan="8">No task to Display</td>
</tr>
</c:if>
<c:if test="${not empty task}">
<c:forEach items="${tasks}" var="task">
<tr class="">
<td>${task.taskid}</td>
<td>${task.taskName}</td>
<td>${task.taskDescription}</td>
<td>
<fmt:message key="task.list.status.text.${task.status}" />
</td>
</tr>
</c:forEach>
</c:if>
</tbody>
</table>
</div>
<%#include file="/WEB-INF/views/includes/footer.jsp"%>
i dont have anything inside my taskRepository atm
For the starters:
#RequestMapping(value = "task", method = RequestMethod.GET)
public String taskList(Map<String, Object> model) {
model.put("task", taskRepository.findAll());
return "/tasks/list";
}
You should return some object you have created instead of String value. Let's asume you want to transfer two fields to you page lets name them field1 and field2. Now create your Data Transfer Object:
public class MyEntityDto{
private String filed1;
private String field2;
//Getter and setter method
.
.
.
}
Now your controller should look something like this:
#Autowired
SomeSevice someService;
#RequestMapping(value = "task", method = RequestMethod.GET)
#ResponseBody
public List<MyEntityDto> taskList(Map<String, Object> model) {
List<MyEntityDto> dtoList = someService.findALl();
return dtoList;
}
Your service from the other hand should look something like this:
#Service
public class SomeService(){
#Autowired
TaskRepository taskRepository;
public List<MyEntityDto> findAll(){
return assemblyTasks(taskRepository.findAll());//TODO implement method assemblyTasks
}
}
Notice that I put your repository usage into the service.This is the way it supposed to be done. You should use services in order to fetch data from your database and than you want to return your data using specificlly design for that purpose object - Data Transfer Object.
I leave the implementation of assemblyTask method to you. What you need to do there is to assign fields you want to pass from entity to view through your dto object. Generally you would want to have an assembler class for every DTO object but for the sake of simplicity I introduced the idea by using method. If you want to read more about DTO, view this post:
getting-value-of-invalid-field-after-methodargumentnotvalidexception
If you are completely new to the Spring world I recommend also find some basics web tutorials, for example here:
gonetoseries
What I cannot figure out is how to select multiple input from multielect drop down to my Action.By using a collections instead of array.I have posted the both jsp code and actionform.
This is my jsp:
<table class="table-striped" style="width: 100%;">
<tr>
<th style="border: none;"><br><br><br><br><label class="control-label" >Grade</label></th>
<th style="border: none;">
<html:select name="GradeBoardConfigureForm" property="grade" multiple="">
<html:option value="">Grade List</html:option>
<html:optionsCollection name="GradeBoardConfigureForm" property="gradelist" label="grade" value="gradeid"/>
</html:select>
</th>
</tr>
</table>
FormBean:
public class GradeBoardConfigureForm extends ActionForm {
private String board;
private List grade;
private List gradelist;
private List boardlist;
public String getBoard() {
return board;
}
public void setBoard(String board) {
this.board = board;
}
public List getGradelist() {
gradelist = new ArrayList<>();
DAOFactory factory = HibernateDAOUtil.getDAOFactory();
GradeDao gradedao = factory.getGradeDao();
List<Academicgradeform> gradedaolist = gradedao.list();
gradelist.addAll(gradedaolist);
return gradelist;
}
public void setGradelist(List gradelist) {
this.gradelist = gradelist;
}
public List getBoardlist() {
boardlist = new ArrayList<>();
DAOFactory factory = HibernateDAOUtil.getDAOFactory();
BoardDao boarddao = factory.getBoardDAO();
List<Academicboardform> boarddaolist = boarddao.list();
boardlist.addAll(boarddaolist);
return boardlist;
}
public void setBoardlist(List boardlist) {
this.boardlist = boardlist;
}
public List getGrade() {
return grade;
}
public void setGrade(List grade) {
this.grade = grade;
}
}
As far as I know it is not directly possible and is not Struts1 philosophy.
A form bean should only be a piece of code that helps to transfer data between the controller (a singleton, so it is stateless) and a view. It is not intended to be a domain object.
So your GradeBoardConfigureForm is a good example of you should not be done. You mix DAO in a form bean. It will be hard to write and hard to test and will lead to poorly structured code. And your problem lays here. If you had a domain object distinct from your form bean, the domain object would have a List, and the DAO layer whould bind the list to the database. And the controller (Action in stuts1) would copy the list to an Array in the form bean, pass it to the view, and in submit phase will use a populated Array according to your needs.
But I have 3 strong advices for you :
Struts1 is deprecated (according to official Apache Struts page), do not use it except for maintaining existing code - Struts2 or Spring MVC are current alternatives
if you are a beginner, start by following existing tutorials
before coding, carefully design your program structure. If you do not understand what I mean here, read (again if you already had a look at it) this document linked at the bottom of Apache strus1 page : Understanding JavaServer Pages Model 2 architecture.