Tuesday, July 18, 2006

Problem with mapping Nested Objects using Struts

I was working on designing a specific requirement where UI was a four step wizard. User will provide information in this four pages and submit the information to be stored in database. The way information was organized on UI (jsp) was different than the way it needed to be represented in DTO (or domain objects).

However, a good review pointed out that use of series of request.getParameter is not good idea. There must be good way to get the data map to Domain objects by using struts. If the data elements were linear ( not mapped and not indexed) Struts does map it quite well with ActionForm. You can use commons beanutils' BeanProperties.copyProperties(Object dest, Object source). But normally the object representation is not linear. Objects have association and multiplicity. (See simple example below)

It seems there is a problem with struts, when it comes to populating action form when jsp contains nested beans (or for that matter indexed beans). It simply does not work. Howerver, using nested tag library, i was able to get the JSP displayed properly with correct parameter name. The problem occurs when i try to submit the JSP page. It throws IllegalArgumentException: No Bean specified. I think I am not doing anything wrong. But, not sure.

Here is what I am trying to do. I have a JSP page with customer information(name, phone, email and address) where address contains(addres1, address2, city, state and zipcode). For purpose of simplicity I have kept all parameters as String. (Couldn't figure out easy way to link files, so I just pasted here.)
Address.java
package com.learning.struts.action;

public class Address {
private String address1;
private String address2;
private String city;
private String state;
private String zipcode;
public String getAddress1() {
return address1;
}
public void setAddress1(String address1) {
this.address1 = address1;
}
public String getAddress2() {
return address2;
}
public void setAddress2(String address2) {
this.address2 = address2;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getZipcode() {
return zipcode;
}
public void setZipcode(String zipcode) {
this.zipcode = zipcode;
}
}

CustomerDTO.java
package com.learning.struts.action;

public class CustomerDTO implements java.io.Serializable{
private String name;
private String phone;
private String email;
private Address address;
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
strust-config.xml
<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"
"http://struts.apache.org/dtds/struts-config_1_2.dtd">

<struts-config>
<form-beans>
<form-bean name="customerForm" type="org.apache.struts.action.DynaActionForm">
<form-property name="page" type="java.lang.String"/>
<form-property name="customer" type="com.learning.struts.action.CustomerDTO"/>
</form-bean>
<form-bean name="customerForm1" type="com.learning.struts.action.PageActionForm">
<form-property name="page" type="java.lang.String"/>
<form-property name="customer" type="com.learning.struts.action.CustomerDTO"/>
</form-bean>
</form-beans>
<global-forwards>
</global-forwards>
<action-mappings>
<action path="/submit"
type="com.learning.struts.action.IndexedNestedFormStrutsAction"
validate="false"
input="customer-address1.jsp"
name="customerForm1">
<forward name="success" path="/jsp/index.jsp"/>
</action>
</action-mappings>
</struts-config></blockquote>


customer-address.jsp

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-nested.tld" prefix="nested" %>
<html:html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>
<html:form action="submit.do" method="post">
<html:hidden property="page" value="page-1"/>
customer.name :<html:text property="customer.name" value="k"/>
customer.phone: </p><html:text property="customer.phone" value="k"/>
customer.email: </p><html:text property="customer.email" value="k"/>
<nested:nest property="customer.address">
customer.address.address1: </p><nested:text property="address1" value="k" />
customer.address.address2: </p><nested:text property="address2" value="k"/>
customer.address.city: </p><nested:text property="city" value="k" />
customer.address.state: </p><nested:text property="state" value="k" />
customer.address.zipcode: </p><nested:text property="zipcode" value="k" />
</nested:nest>
<html:submit></html:submit>
</html:form>
</body>
</html:html>
IndexedNestedFormStrutsAction.java

public class IndexedNestedFormStrutsAction extends Action {

public ActionForward execute(ActionMapping map, ActionForm form, HttpServletRequest req, HttpServletResponse res) throws Exception {

DynaActionForm daForm = (DynaActionForm)form;
CustomerDTO customer = (CustomerDTO)daForm.get("customer");
System.out.println("Customer : " + customer);
return map.findForward("success");
}
}

On debugging this, it throws up folloing exception :

java.lang.IllegalArgumentException: No bean specified
at org.apache.commons.beanutils.PropertyUtilsBean.getPropertyDescriptor(PropertyUtilsBean.java:751)
at org.apache.commons.beanutils.BeanUtilsBean.setProperty(BeanUtilsBean.java:937)
at org.apache.commons.beanutils.BeanUtilsBean.populate(BeanUtilsBean.java:811)
at org.apache.commons.beanutils.BeanUtils.populate(BeanUtils.java:298)
at org.apache.struts.util.RequestUtils.populate(RequestUtils.java:493)
at org.apache.struts.action.RequestProcessor.processPopulate(RequestProcessor.java:816)
at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:203)
at org.apache.struts.action.ActionServlet.process(ActionServlet.java:1196)
at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:709)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:802)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:252)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:173)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:213)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:178)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:126)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:105)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:107)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:148)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:869)
at org.apache.coyote.http11.Http11BaseProtocol$Http11ConnectionHandler.processConnection(Http11BaseProtocol.java:664)
at org.apache.tomcat.util.net.PoolTcpEndpoint.processSocket(PoolTcpEndpoint.java:527)
at org.apache.tomcat.util.net.LeaderFollowerWorkerThread.runIt(LeaderFollowerWorkerThread.java:80)
at org.apache.tomcat.util.threads.ThreadPool$ControlRunnable.run(ThreadPool.java:684)
at java.lang.Thread.run(Thread.java:595)

But I thought that it may be problem with DynaActionForm so I created ActionForm derivative and try if it properly maps the nested bean. Here is the code for that:

PageActionFrom.java
package com.learning.struts.action;

import org.apache.struts.action.ActionForm;

public class PageActionForm extends ActionForm {
private String page;
private CustomerDTO customer;
public CustomerDTO getCustomer() {
return customer;
}
public void setCustomer(CustomerDTO customer) {
this.customer = customer;
}
public String getPage() {
return page;
}
public void setPage(String page) {
this.page = page;
}
}

But it does not work and though it generated the JSP correctly. When you do view source on generated html it will create proper dot notaion name for each input element. However, submitting and getting the values even inside ActionForm parameter of Action does not work.
I did find out some open source project in works which are working to deal with this problem. But they are in still 0.x version. I think this is something struts should do automatically rather than any plugin framework. Please correct me if I am wrong.

2 comments:

Jatan Porecha said...

The error you were getting because Address object in CustomerDTO would be NULL if CustomerDTO object is freshly created.
When Struts creats object of CustomerDTO, it would not have any way how to create object for Address and therefore the getAddress invocation would return NULL, which ultimately results in to said error.

Anonymous said...

My name is Kimberly Gonzales I was browsing internet and found your blog. The author did a great job. I will subscribe to your RSS feeds. Thank you for your contribution. I am a web designer myself. And here some examples of the websites that I designed for bad credit loans payday advance company.