I am beginner in Struts2, web development. To learn, I am developing a recipe-store application. I can CRUD base materials into a MySQL database with JDBC. Now I have a JSP page where user can create ingredients from base materials to form a new recipe. In my program an ingredient is a base material which has quantity and a unit name set. (Beforehand when user creates base materials he can specify one additional unit name (e.g. cup, teaspoon, glass etc.) beside the default unit names (g, dkg, kg)).
I have a Struts2 doubleselect
where the first select holds base material names and the second holds the relevant unitnames belonging to the selected base material. I have quantity input textfield as well.
From these 3 inputs I can create and add dynamically a row of new ingredient to a HTML table in JSP. The inputs in the table are hidden text inputs appended to cells after the visible cell values. They are hidden because I don't want them to be editable and disabled non-editable textfields not allow submitting their values. Also, every row gets a delete anchor upon creation. The indexes in the names of the hidden input elements are recalculated after every deletion. Click me for the view of new recipe creation JSP
I can submit the inputs, accept and print it with struts2 action in result JSP when there is no deletion of rows, or the the row deleted is the last one. Otherwise, if I delete a row in the middle or at the beginning of the dynamic ingredient list, I get a NullPointerException
after form submission. If I delete the last row and submit after, it works perfectly, just like if I am not deleting any rows.
So, what am I missing here? Am I not properly deleting the table row? How am I supposed to do that?
Relevant parts of newrecipe.jsp
:
<script>
function addRow(tableID) {
//get material quantity value from quantity field
var materialQuantity = document.getElementById("materialQuantity");
/*VALIDATION OF QUANTITY FIELD: must be number, cannot be empty
[better to turn off validation until development is ongoing,
this is just set here for clarity while asking for help]*/
if (materialQuantity.value == "") {
alert("Add meg a hozzávaló mennyiségét!");
}
else if (isNaN(materialQuantity.value) == true) {
alert("A mennyiség csak szám lehet!");
}
else {
//get selected material name value
var selectedMaterial = $("#recipeSelect1 :selected").text()
//get selected unit name value
var selectedUnitName = $("#recipeSelect2 :selected").text()
//get table object
var table = document.getElementById(tableID);
var rowCount = table.rows.length;
var row = table.insertRow(rowCount);
var counts=rowCount;
/*create the cells of a new ingredient dynamic table row and
insert, append a hidden text input into it beside the visible values*/
var cell1 = row.insertCell(0);
var ingredientName = document.createElement("input");
ingredientName.type = "hidden";
ingredientName.name="recipe.ingredients["+counts+"].ingredientName";
ingredientName.value=selectedMaterial;
$(cell1).append(ingredientName);
$(cell1).append(selectedMaterial);
var cell2 = row.insertCell(1);
var ingredientUnitName = document.createElement("input");
ingredientUnitName.type = "hidden";
ingredientUnitName.name="recipe.ingredients["+counts+"].ingredientUnitName";
ingredientUnitName.value=selectedUnitName;
ingredientUnitName.className="materialFields";
$(cell2).append(ingredientUnitName);
$(cell2).append(selectedUnitName);
var cell3 = row.insertCell(2);
var ingredientQuantity = document.createElement("input");
ingredientQuantity.type = "hidden";
ingredientQuantity.name="recipe.ingredients["+counts+"].ingredientQuantity";
ingredientQuantity.value=materialQuantity.value;
ingredientQuantity.className="materialFields";
$(cell3).append(ingredientQuantity);
$(cell3).append(materialQuantity.value);
//create the dynamic anchor element for deletion of table row
var cell4 = row.insertCell(3);
var delIR = document.createElement("a");
delIR.href = "#";
delIR.className = "delIR";
delIR.innerHTML="Törlés";
delIR.onclick = function () {
//delete row
$(cell4).closest('tr').remove();
//recalculate name index after deletion of a row
for(var i=0; i < table.rows.length; i++) {
var inp0 = table.rows[i].cells[0].getElementsByTagName("input");
inp0.name = "recipe.ingredients["+i+"].ingredientName";
var inp1 = table.rows[i].cells[1].getElementsByTagName("input");
inp1.name="recipe.ingredients["+i+"].ingredientUnitName";
var inp2 = table.rows[i].cells[2].getElementsByTagName("input");
inp2.name="recipe.ingredients["+i+"].ingredientQuantity";
table.rows[i].cells[4].innerHTML = inp0.name;
}
};
$(cell4).append(delIR);
//print ingredient name for test purposes
var cell5 = row.insertCell(4);
$(cell5).append(ingredientName.name);
//empty quantity field after creation of row
materialQuantity.value = "";
}
}
</script>
</head>
<body>
<h1><s:property value="#materialTitle"/></h1>
<s:form action="saverecipe" method="post" id="recipeForm" theme="simple">
<div class="recipeLeftDiv">
<div class="recipeNameDiv">
<label class="recipeLabel" for="recipeNameField">Recept neve:</label>
<s:textfield
cssClass="recipeNameField"
id="recipeNameField"
name="recipe.recipeName"
/>
</div>
<s:doubleselect
id="recipeSelect1"
cssClass="recipeSelect1"
name="recipeSelect1"
list="materialNameUnitNameMap.keySet()"
doubleId="recipeSelect2"
doubleCssClass="recipeSelect2"
doubleName="recipeSelect2"
doubleList="materialNameUnitNameMap.get(top)"
theme="css_xhtml"
>
</s:doubleselect>
<s:textfield
id="materialQuantity"
cssClass="materialFields"
>
</s:textfield>
<button type="button" onclick="addRow('ingredientTable')">Hozzáadás</button>
<table id="ingredientTable" class="ingredientTable">
</table>
</div>
SaveRecipeAction.java
:
package actions;
import com.opensymphony.xwork2.ActionSupport;
import beans.Ingredient;
import beans.Recipe;
public class SaveRecipeAction extends ActionSupport {
private Recipe recipe;
public Recipe getRecipe() {
return recipe;
}
public void setRecipe(Recipe recipe) {
this.recipe = recipe;
}
public String execute() {
System.out.println("a hozzávalók lista mérete: "+recipe.getIngredients().size());
for (Ingredient ing:recipe.getIngredients()) {
System.out.print(ing.getIngredientName()+" ");
System.out.print(ing.getIngredientQuantity()+" ");
System.out.println(ing.getIngredientUnitName());
}
return SUCCESS;
}
}
Recipe.java
:
package beans;
import java.util.List;
public class Recipe {
private String recipeName;
private List<Ingredient> ingredients;
private String recipeDesc;
public String getRecipeName() {
return recipeName;
}
public void setRecipeName(String recipeName) {
this.recipeName = recipeName;
}
public List<Ingredient> getIngredients() {
return ingredients;
}
public void setIngredients(List<Ingredient> ingredients) {
this.ingredients = ingredients;
}
public String getRecipeDesc() {
return recipeDesc;
}
public void setRecipeDesc(String recipeDesc) {
this.recipeDesc = recipeDesc;
}
}
Ingredient.java
:
package beans;
public class Ingredient {
private double ingredientQuantity;
private String ingredientName;
private String ingredientUnitName;
public String getIngredientName() {
return ingredientName;
}
public void setIngredientName(String ingredientName) {
this.ingredientName = ingredientName;
}
public double getIngredientQuantity() {
return ingredientQuantity;
}
public void setIngredientQuantity(double ingredientQuantity) {
this.ingredientQuantity = ingredientQuantity;
}
public String getIngredientUnitName() {
return ingredientUnitName;
}
public void setIngredientUnitName(String ingredientUnitName) {
this.ingredientUnitName = ingredientUnitName;
}
}
Edit1: stacktrace It says to me, that it does not find the name of an ingredient (the deleted one) in the list of the ingredients while I am trying to print that name out for testing purposes in the SaveRecipeAction
. And this means, that upon submission of the form it will try to set, wrong number of ingredients. It tries to set number of ingredients to the list according to the "earlier / before deletion state"-length of table.
Say, if I got 3 items in ingredient-list like on the screenshot I attached and delete the middle one, and submit form after that, it will print only the first element. First I thought, the reason behind this is because when I test the program I navigate back to previous page, and by that time the list is already submitted once I should delete deeper (in the list in the recipe bean as well). That shouldn't even happen since JS script is just client side. But this problem occurs even after a fresh restart of tomcat, reload of page.
Struts Problem Report Struts has detected an unhandled exception: Messages: File: actions/SaveRecipeAction.java Line number: 21 Stacktraces java.lang.NullPointerException actions.SaveRecipeAction.execute(SaveRecipeAction.java:21) sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) java.lang.reflect.Method.invoke(Method.java:606) com.opensymphony.xwork2.DefaultActionInvocation.invokeAction(DefaultActionInvocation.java:450) com.opensymphony.xwork2.DefaultActionInvocation.invokeActionOnly(DefaultActionInvocation.java:289) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:252) org.apache.struts2.interceptor.debugging.DebuggingInterceptor.intercept(DebuggingInterceptor.java:256) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor.doIntercept(DefaultWorkflowInterceptor.java:167) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.validator.ValidationInterceptor.doIntercept(ValidationInterceptor.java:265) org.apache.struts2.interceptor.validation.AnnotationValidationInterceptor.doIntercept(AnnotationValidationInterceptor.java:68) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ConversionErrorInterceptor.intercept(ConversionErrorInterceptor.java:138) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:239) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ParametersInterceptor.doIntercept(ParametersInterceptor.java:239) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.StaticParametersInterceptor.intercept(StaticParametersInterceptor.java:191) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) org.apache.struts2.interceptor.MultiselectInterceptor.intercept(MultiselectInterceptor.java:73) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) org.apache.struts2.interceptor.CheckboxInterceptor.intercept(CheckboxInterceptor.java:91) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) org.apache.struts2.interceptor.FileUploadInterceptor.intercept(FileUploadInterceptor.java:252) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor.intercept(ModelDrivenInterceptor.java:100) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor.intercept(ScopedModelDrivenInterceptor.java:141) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ChainingInterceptor.intercept(ChainingInterceptor.java:145) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.PrepareInterceptor.doIntercept(PrepareInterceptor.java:171) com.opensymphony.xwork2.interceptor.MethodFilterInterceptor.intercept(MethodFilterInterceptor.java:98) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.I18nInterceptor.intercept(I18nInterceptor.java:161) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) org.apache.struts2.interceptor.ServletConfigInterceptor.intercept(ServletConfigInterceptor.java:164) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.AliasInterceptor.intercept(AliasInterceptor.java:193) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor.intercept(ExceptionMappingInterceptor.java:189) com.opensymphony.xwork2.DefaultActionInvocation.invoke(DefaultActionInvocation.java:246) org.apache.struts2.impl.StrutsActionProxy.execute(StrutsActionProxy.java:54) org.apache.struts2.dispatcher.Dispatcher.serviceAction(Dispatcher.java:563) org.apache.struts2.dispatcher.ng.ExecuteOperations.executeAction(ExecuteOperations.java:77) org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter.doFilter(StrutsPrepareAndExecuteFilter.java:99) org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243) org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210) org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222) org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123) org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502) org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171) org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100) org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953) org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118) org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408) org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1041) org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:603) org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:312) java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145) java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615) java.lang.Thread.run(Thread.java:744)
Edit 2: Output of the first line of the execution method of SaveRecipeAction action, in case of first deleting the middle ingredient from the sample 3-rows-long table and then submitting the form:
a hozzávalók lista mérete: 3
that is, the "size of the ingredient-list is: 3"
Edit 3: After modifying the index-recalculate block with alert(inp0.length)
it will output 1
two times.
//recalculate name index after deletion of a row
for(var i=0; i < table.rows.length; i++) {
var inp0 = table.rows[i].cells[0].getElementsByTagName("input");
inp0.name = "recipe.ingredients["+i+"].ingredientName";
alert(inp0.length);
var inp1 = table.rows[i].cells[1].getElementsByTagName("input");
inp1.name="recipe.ingredients["+i+"].ingredientUnitName";
var inp2 = table.rows[i].cells[2].getElementsByTagName("input");
inp2.name="recipe.ingredients["+i+"].ingredientQuantity";
table.rows[i].cells[4].innerHTML = inp0.name;
}
Edit 4:alert(inp0[0].length)
outputs "undefined". inp0[1].length
and indexes above (checked till index 5
), do not output alertbox at all.
execute
method? – Roman Calert(inp0.length)
? – Roman Cinp0
toinp0[0]
and other similar vars? – Roman C