Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

org.springframework.beans.NullValueInNestedPathException: auto-grow nested property path in Spring MVC 3.2.8

I have a project based in Spring Web model-view-controller (MVC) framework. The version of the Spring Web model-view-controller (MVC) framework is 3.2.8.

This class

public class DeviceForm {

    Device device;

    List<String> selectedItems = Collections.emptyList();

    public DeviceForm() {
        super();
    }

    public Device getDevice() {
        return device;
    }

    public void setDevice(Device device) {
        this.device = device;
    }

    public List<String> getSelectedItems() {
        return selectedItems;
    }

    public void setSelectedItems(List<String> selectedItems) {
        this.selectedItems = selectedItems;
    }


}

and this

public class Device implements java.io.Serializable {

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "CRITERIA")
    private BaseCriteria criteria;

    public BaseCriteria getCriteria() {
        return criteria;
    }

    public void setCriteria(BaseCriteria criteria) {
        this.criteria = criteria;
    }
}

and this

@Entity
@Table(name = "CRITERIA")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DISCRIMINATOR", discriminatorType = DiscriminatorType.STRING)
@SequenceGenerator(name = "seqCriteria", sequenceName = "SEQ_CRITERIA", allocationSize = 1)
public abstract class BaseCriteria {

    public BaseCriteria() {
        super();
    }   

    private Long id;
    private String code;
    private Date adoptionDate;
    private Date expirationDate;

    @Transient
    public abstract String getGroupKey();

    @Transient
    public abstract Long getGroupId();

    @Transient
    public abstract String getRefColumnName();

    @Id
    @Column(name = "ID", unique = true, nullable = true)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "seqCriteria")
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Column(name = "CODE")
    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    @Column(name = "ADOPTION_DATE")
    @Temporal(TemporalType.TIMESTAMP)
    public Date getAdoptionDate() {
        return adoptionDate;
    }

    public void setAdoptionDate(Date adoptionDate) {
        this.adoptionDate = adoptionDate;
    }

    @Column(name = "EXPIRATION_DATE")
    @Temporal(TemporalType.TIMESTAMP)
    public Date getExpirationDate() {
        return expirationDate;
    }

    @Transient
    public boolean isExpired() {
        return getExpirationDate().before(new Date());
    }

    public void setExpirationDate(Date expirationDate) {
        this.expirationDate = expirationDate;
    }


    @Override
    public String toString() {
        return "BaseCriteria [id=" + id + ", code=" + code + ", adoptionDate="
                + adoptionDate + ", expirationDate=" + expirationDate + "]";
    }


}

and the JSP

<form:form  commandName="deviceForm"
             name="deviceForm"
             id="deviceFormId" 
             method="post"
             action="${contextPath}/newdesign/manage/device/${deviceForm.device.id}"
             htmlEscape="yes">

 <div class="col-sm-6 text-right">
     <button class="btn btn-primary" type="submit">Save device</button>
</div>
</div>

<c:forEach items="${deviceForm.device.productGroup.criteria}" var="criteria">                                                
     <div class="row">                                                                                                 
            <div class="col-md-3">
                <form:radiobutton path="device.criteria.id" value="${criteria.id}"/>
                <label for="basic-url">Criteria:</label>
                <input value="${criteria.code}" disabled="disabled" class="form-control"/>
            </div>
            <div class="col-md-3">                                                                                                              
                <label for="basic-url">Adoption date:</label>
                <input value="<fmt:formatDate type="date" value="${criteria.adoptionDate}" />"      disabled="disabled" class="form-control"/>
            </div>                
            <div class="col-md-3">                                                                                              
                <label for="basic-url">Expiration Date:</label>
                <input value="<fmt:formatDate type="date" value="${criteria.expirationDate}" />"    disabled="disabled" class="form-control"/>
            </div>                                                                                                                                            
    </div>
</c:forEach>
</form:form>

The controller:

/**
     * @throws Exception    
     *                                 
     */
    @RequestMapping(value = {       "/newdesign/manage/device/{appId}",
                                    "/newdesign/manage/device/{appId}/"}, method = {RequestMethod.GET})
    public String viewDevicesWithStatus(                        
                                     @ModelAttribute("deviceForm") DeviceForm deviceForm,
                                     @PathVariable Long appId,                                   
                                     HttpServletRequest request,
                                     Model model ) throws Exception {

        Device device =   manageLicenseService.getDeviceById(appId, true);

        if (device.getCriteria()==null) {
            device.setCriteria(device.getProductGroup().getCriteria().get(0));
        }

        deviceForm.setDevice(device);       
        fillModel (model, request, device);

        return "cbViewDeviceInfo";      
    }

    /**
     * @throws Exception    
     *                                 
     */
    @RequestMapping(value = {       "/newdesign/manage/device/{appId}",
                                    "/newdesign/manage/device/{appId}/"}, method = {RequestMethod.POST})
    public String saveDevicesWithStatus(                                
                                     @ModelAttribute("deviceForm") DeviceForm deviceForm,
                                     @PathVariable Long appId,                                   
                                     HttpServletRequest request,
                                     Model model ) throws Exception {           

        Device device =   manageLicenseService.getDeviceById(deviceForm.getDevice().getId());

        if (device.getCriteria()==null) {
            device.setCriteria(device.getProductGroup().getCriteria().get(0));
        }

        //TODO: audit
        device.updateDevice(deviceForm.getDevice());
        manageLicenseService.saveDevice(device);    

        if (device.getCriteria()==null) {
            device.setCriteria(device.getProductGroup().getCriteria().get(0));
        }

        deviceForm.setDevice(device);
        fillModel (model, request, device);


        return "cbViewDeviceInfo";      
    }

But I got following error when I submitted the form, on GET method I got same page without error

org.springframework.beans.NullValueInNestedPathException: Invalid property 'device.criteria' of bean class [com.tdk.iot.controller.newdesign.manage.DeviceForm]: Could not instantiate property type [com.tdk.iot.domain.criteria.BaseCriteria] to auto-grow nested property path: java.lang.InstantiationException
like image 265
Amadeu Cabanilles Avatar asked Jan 10 '17 13:01

Amadeu Cabanilles


1 Answers

You get the error because in your form you have this:

<form:radiobutton path="device.criteria.id" value="${criteria.id}"/>

and in your POST handler you have this:

public String saveDevicesWithStatus(@ModelAttribute("deviceForm") DeviceForm deviceForm){

}

which means that the MVC framework will try to automatically set the property

deviceForm.device.criteria.id.

Now, because there is no existing DeviceForm in any scope then it will create a new one and of course device.getCriteria() returns null, hence the exception.

You may think that the DeviceForm you created and populated in the GET handler will be used however Spring MVC is stateless so you would need to store it in Session scope between requests for it to be re-used or otherwise rework your logic.

https://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-modelattrib-method-args

.... Given the above example where can theinstance come from? There are several options.....[in the absence of any other option] It may be instantiated using its default constructor

A better approach however is to change your form to be as below:

<form:radiobutton path="device.criteria" value="${criteria.id}"/>

and register a converter that would convert the submitted parameter and bind the corresponding entity instance.

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/validation.html#core-convert

@Component
public class StringToCriteriaConverter implements Converter<String, BaseCriteria> {

    @Autowired
    private CriteriaService service;

    //source is the ID passed by the page
    public BaseCriteria convert(String source) {
        // lookup and return item with corresponding ID from the database
    }
}
like image 186
Alan Hay Avatar answered Oct 23 '22 04:10

Alan Hay