MultiActionFormController in Spring

I last felt the requirement of a Controller that could process the form submitted as well as the view page be able to trigger multiple events triggered to the same controller.
It was simply a burden to have both feature in  a single controller. But after searching and some re-searching(ha..ha) I came out for the solution I required.
To have a controller that can process the form as well as has multiple methods that will be triggered as my requirement.

For this I made a class called SimpleMultiActionFormController as shown below:


import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.springframework.validation.BindException;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;

import com....api.service.ProductionManager;
import com...impl.dto.ProductionManagerDTO;
import com......impl.dto.UserEventLogDTO;
import com....impl.service.ProductionManagerImpl;

/**
 * <desc>This method will be responsible to act as combination of
 * SimpleFormController and MultiActionController</desc>
 * @author Ramesh Raj Baral
 * @since:Jan 21, 2010
 * File:SimpleMultiActionFormController.java
 * @Version:1.0
 */

public class SimpleMultiActionFormController extends SimpleFormController {
   
    private MethodNameResolver methodNameResolver = new SubmitParameterPropertiesMethodNameResolver();
    private ProductionManager productionManager;
    private String fileUploadPath;
    private Logger log=Logger.getLogger(this.getClass());
   
    public final void setMethodNameResolver(MethodNameResolver methodNameResolver) {
        this.methodNameResolver = methodNameResolver;
    }
   
    public final MethodNameResolver getMethodNameResolver() {
        return this.methodNameResolver;
    }
    protected Object formBackingObject(HttpServletRequest request) throws ServletException {
            return productionManager; 
        }
   
    public ProductionManager getProductionManager() {
        return productionManager;
    }

    public void setProductionManager(ProductionManager productionManager) {
        this.productionManager = productionManager;
    }

    public String getFileUploadPath() {
        return fileUploadPath;
    }

    public void setFileUploadPath(String fileUploadPath) {
        this.fileUploadPath = fileUploadPath;
    }
   
    protected ModelAndView processFormSubmission(HttpServletRequest request,
            HttpServletResponse response, Object command, BindException errors)
            throws Exception {
        ProductionManagerDTO pdto=(ProductionManagerDTO)((ProductionManagerImpl)command).getModel();
        log.info("binded filename from parent:"+pdto.getFileName()+"...");
        ServletRequestDataBinder binder = createBinder(request, command);
        binder.bind(request);

       
        if (errors.hasErrors() || isFormChangeRequest(request)) {
            if (logger.isDebugEnabled()) {
                logger.info("Data binding errors: " + errors.getErrorCount());
            }
            logger.info("error is:"+errors.getFieldValue(request.getAttribute("file0").toString()));
            logger.info("error is:"+errors.getNestedPath());
           
            return showForm(request, response, errors);
        }
        else {
            String methodName = this.methodNameResolver.getHandlerMethodName(request);
           
            Method m = (Method) this.getClass().getMethod(methodName,
                    new Class[] {HttpServletRequest.class,HttpServletResponse.class,
                                 Object.class,BindException.class});
            if (m == null) {
                throw new NoSuchRequestHandlingMethodException(methodName, getClass());
            }
           

            List params = new ArrayList(4);
            params.add(request);
            params.add(response);
            params.add(command);
            params.add(errors);
           
            return (ModelAndView) m.invoke(this, params.toArray(new Object[params.size()]));
            //return null;
        }
       
    }

    public ModelAndView deleteFiles(HttpServletRequest request,HttpServletResponse response, Object command, BindException errors) throws Exception {
        logger.info("inside deleteFiles method...........");
        ProductionManagerDTO pdto=(ProductionManagerDTO)((ProductionManagerImpl)command).getModel();
        System.out.println("binded filename:"+pdto.getFileName()+"...");
        String file = request.getParameter("file");
        String fileName = request.getParameter("fileName");
        String fileID=request.getParameter("fileID");
        String tempDestPath=new File(new File(".").getCanonicalPath()).toString()+System.getProperty("file.separator")+this.fileUploadPath+System.getProperty("file.separator")+fileName;
        File uFile = new File(tempDestPath);
        System.out.println("filepath is:"+tempDestPath);
        System.out.println("File exist:"+uFile.exists()+"..readable:"+uFile.isFile());
       
        return this.showForm(request, errors, getFormView());
    }
   
    public ModelAndView finalSubmit(HttpServletRequest request,HttpServletResponse response, Object command, BindException errors) throws Exception {
        Map model = new HashMap();
        model.put("item", "");
        logger.info("inside finalSubmit method");
        logger.info("inside deleteFiles method...........");
        ProductionManagerDTO pdto=(ProductionManagerDTO)((ProductionManagerImpl)command).getModel();
        System.out.println("binded filename:"+pdto.getFileName()+"...");
        String file = request.getParameter("file");
        String fileName = request.getParameter("fileName");
        String fileID=request.getParameter("fileID");
        String tempDestPath=new File(new File(".").getCanonicalPath()).toString()+System.getProperty("file.separator")+this.fileUploadPath+System.getProperty("file.separator")+fileName;
        File uFile = new File(tempDestPath);
        System.out.println("filepath is:"+tempDestPath);
        System.out.println("File exist:"+uFile.exists()+"..readable:"+uFile.isFile());
        return new ModelAndView(getSuccessView(), model);
    }
 
This has the properties as normal and their getter and setters. The point to be noted is the method processFormSubmission(...). this method will return the name of the corresponding method whose name is posted in request scope.
The child class of this class can have multiple methods. The method names are resolved using the methodResolver.

Now register your customized resolver in your dispatcher servlet as shown below in mine case:


<bean id="submitActionParamResolver" class="com....impl.web.controller.SubmitParameterPropertiesMethodNameResolver">
       <property name="mappings">
          <props>
            <prop key="_finish">finalSubmit</prop>
            <prop key="_deleteFiles">deleteFiles</prop>
            <prop key="_addFileElement">addElement</prop>
            <prop key="_removeFileElement">removeElement</prop>
         </props>
       </property>
       <property name="defaultMethodName"><value>finalSubmit</value></property>
</bean>







There are four methods registered namely finalSubmit,deleteFiles,addElement,removeElement. The SubmitParameterPropertiesMethodNameResolver is shown below:













import java.util.Iterator;
import java.util.Properties;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;
import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
import org.springframework.web.util.WebUtils;
/**
 * <desc>This class resolves the method name from the request parameter
 * and provide its mapped value defined in dispatcher servlet</desc>
 * @author Ramesh Raj Baral
 * @since:Jan 21, 2010
 * File:SubmitParameterPropertiesMethodNameResolver.java
 * @Version:1.0
 */
public class SubmitParameterPropertiesMethodNameResolver implements
MethodNameResolver {

private String defaultMethodName;

private Properties mappings;
private Logger logger=Logger.getLogger(this.getClass());

/**
* Set URL to method name mappings from a Properties object.
* @param mappings properties with URL as key and method name as value
*/
public void setMappings(Properties mappings) {
this.mappings = mappings;
}

/**
* @param defaultMethodName The defaultMethodName to set.
*/
public void setDefaultMethodName(String defaultMethodName) {
    //logger.info("setting defaultMethod:"+defaultMethodName);
this.defaultMethodName = defaultMethodName;
}

public void afterPropertiesSet() {
if (this.mappings == null || this.mappings.isEmpty()) {
    throw new IllegalArgumentException("'mappings' property is required");
}
}

public String getHandlerMethodName(HttpServletRequest request)
    throws NoSuchRequestHandlingMethodException {

for (Iterator it = this.mappings.keySet().iterator(); it.hasNext();) {
    String submitParamter = (String) it.next();
    //logger.info("getHandlerMethodName:"+submitParamter);
    if (WebUtils.hasSubmitParameter(request, submitParamter)) {
        return (String) this.mappings.get(submitParamter);
    }
}
//logger.info("returning defaultMethodName");
return defaultMethodName;
}
}





Now your Controller will be as shown below:

package com.d2.vhippm.impl.web.controller;

import java.io.File;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.ModelAndView;



/**
 * This is responsible for the actions
 * <li>fileview</li>
 * <li>fieldelete</li>
 * <li>comment related actions</li>
 * @author Ramesh Raj Baral
 * @since:Jan 22, 2010
 * File:CommentsController.java
 * @Version:1.0
 */
public class CommentsController extends SimpleMultiActionFormController {

    /** Logger for this class and subclasses */
    protected final Log logger = LogFactory.getLog(getClass());
    private ProductionManager productionManager;
    private List<DTO> prodManagerFilesList;
    private ProductionManagerFilesDTO prodManagerFile;
    private UserEventLogDTO userEventDTO;
    private List<DTO> userEventLogsList;
    private String fileUploadPath;
   
   
     protected Object formBackingObject(HttpServletRequest request) throws ServletException {
        return productionManager; 
    }

    public ProductionManager getProductionManager() {
        return productionManager;
    }

    public void setProductionManager(ProductionManager productionManager) {
        this.productionManager = productionManager;
    }

    public ModelAndView deleteFiles(HttpServletRequest request,HttpServletResponse response, Object command, BindException errors) throws Exception {
        logger.info("inside deleteFiles method");
        ProductionManagerDTO pdto=(ProductionManagerDTO)((ProductionManagerImpl)command).getModel();
        ProductionManagerFilesDTO selectedFileDTO=(ProductionManagerFilesDTO)pdto.getSelectedFileDTO();
        String fileLocation = selectedFileDTO.getFileLocation();
        String fileName = selectedFileDTO.getFileName();
        String fileID=selectedFileDTO.getFileID();
        String tempDestPath=new File(new File(".").getCanonicalPath()).toString()+System.getProperty("file.separator")+this.fileUploadPath+System.getProperty("file.separator")+fileName;
        File uFile = new File(tempDestPath);
        System.out.println("filepath is:"+tempDestPath);
        System.out.println("File exist:"+uFile.exists()+"..readable:"+uFile.isFile());
        if(uFile.delete()){
            userEventDTO=new UserEventLogDTO();
            userEventDTO.setLogDesc("File_Deleted:"+tempDestPath);
            userEventDTO.setUserID("19345");
            userEventDTO.setUserName("system");
            userEventDTO.setLogType("File_Deleted");
            userEventDTO.setClientGroupID(1);
            userEventDTO.setClientID("1");
            this.productionManager.logEvents(userEventDTO);
            this.productionManager.updateModel(selectedFileDTO);
        }
       
        return this.showForm(request, errors, getFormView());
    }

    public ModelAndView finalSubmit(HttpServletRequest request,HttpServletResponse response, Object command, BindException errors) throws Exception {
        logger.info("inside finalSubmit method");
        ProductionManagerDTO pdto=(ProductionManagerDTO)((ProductionManagerImpl)command).getModel();
        pdto.getNewCommentsDTO().setFileUploadPath(fileUploadPath);
        MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;
        final Map files = multiRequest.getFileMap();
        System.out.println(fileUploadPath);
        if (files == null) {
              logger.info("no file was sent");
        }
        else{  
            String tempDestPath;
            Iterator fileIterator=files.values().iterator();
            MultipartFile file;
            prodManagerFilesList=new ArrayList<DTO>();
            userEventLogsList=new ArrayList<DTO>();
            while(fileIterator.hasNext()) {
                tempDestPath=new File(new File(".").getCanonicalPath()).toString()+System.getProperty("file.separator")+this.fileUploadPath+System.getProperty("file.separator");
                System.out.println("uploading to:"+tempDestPath);
                file =(MultipartFile)fileIterator.next();
                tempDestPath+=file.getOriginalFilename();
                prodManagerFile=new ProductionManagerFilesDTO();
                prodManagerFile.setFile(file);
                prodManagerFile.setIsDeleted('N');
                prodManagerFile.setFileName(file.getOriginalFilename());
                prodManagerFile.setFileSize(file.getSize());
                prodManagerFile.setFileType(file.getContentType());
                prodManagerFile.setUploadedBy("currentUser");
                prodManagerFile.setUploadedOn(new Date());
                prodManagerFile.setType(file.getContentType());
                if(StringUtils.hasLength(file.getOriginalFilename()) && file.getSize()>0){
                    prodManagerFilesList.add(prodManagerFile);
                }
                //file.transferTo(new File(tempDestPath));
                //make an entry if the transfer succeeds
            }
            pdto.getNewCommentsDTO().setFileDTOList(prodManagerFilesList);
        }
        if(StringUtils.hasLength(StringUtils.trimAllWhitespace(pdto.getNewCommentsDTO().getCommentMessage())))
            this.productionManager.updateModel(pdto);
        ((ProductionManagerDTO)this.productionManager.getModel()).getNewCommentsDTO().setCommentMessage("");
        List fileDTOList=new ArrayList();
        fileDTOList.add(new ProductionManagerFilesDTO());
        ((ProductionManagerDTO)this.productionManager.getModel()).getNewCommentsDTO().setFileDTOList(fileDTOList);
        return this.showForm(request, errors, getSuccessView());
    }
    /* (non-Javadoc)
     * @see org.springframework.web.servlet.mvc.SimpleFormController#referenceData(javax.servlet.http.HttpServletRequest)
     */
    protected Map referenceData(HttpServletRequest request) throws Exception {
        logger.info("inside referencedata");
        return super.referenceData(request);
    }
    public ModelAndView addElement(HttpServletRequest request,HttpServletResponse response, Object command, BindException errors) throws Exception {
        logger.info("adding new file element");
        this.productionManager.addElement(((ProductionManagerDTO)this.productionManager.getModel()).getNewCommentsDTO().getFileDTOList());
        return this.showForm(request, errors, getFormView());
       
    }
   
    public ModelAndView removeElement(HttpServletRequest request,HttpServletResponse response, Object command, BindException errors) throws Exception {
        logger.info("removing file element");
        return this.showForm(request, errors, getFormView());
        /**
        if(((ProductionManagerDTO)this.productionManager).getNewCommentsDTO().getFileDTOList().size()>1)
        return this.productionManager.removeElement(((ProductionManagerDTO)this.productionManager).getNewCommentsDTO().getFileDTOList(), 1);
        else
            return ((ProductionManagerDTO)this.productionManager).getNewCommentsDTO().getFileDTOList();
            */
    }
   
   
    public String getFileUploadPath() {
        return fileUploadPath;
    }

    public void setFileUploadPath(String fileUploadPath) {
        this.fileUploadPath = fileUploadPath;
    }
}

As you can see this controller has different methods. Register this controller in your dispatcher servlet as well:
<bean id="commentsController" class="com.....impl.web.controller.CommentsController">
    <property name="methodNameResolver"><ref bean="submitActionParamResolver"/></property>
    <property name="sessionForm"><value>true</value></property>
    <property name="commandName" value="productionManager"/>
    <property name="formView" value="commentPost"/>
    <property name="successView" value="PMCycles"/>
    <property name="productionManager" ref="productionManager"/>
    <property name="fileUploadPath" value=".../Clients"/>
</bean>


Now your jsp page will be something like:


<%@ include file="/VHIPPM/jsp/include.jsp"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

<html>
<head>
<script type="text/javascript">
  function setFileParameters(fileLocation,fileID,fileName,buttonName){
                      document.getElementById("fileID").value=fileID;
                      document.getElementById("fileName").value=fileName;
                      document.getElementById("fileLocation").value=fileLocation;
                      document.fileViewForm.fileName.value=fileName;
                      document.fileViewForm.fileID.value=fileID;
                      document.fileViewForm.fileLocation.value=fileLocation;
                    
                      document.fileViewForm._deleteFiles.click();
                      document.fileViewForm.submit();

                    alert("setting fileParam"+fileLocation+".."+fileID+".."+fileName+".."+ buttonName);
                      //return true;
                  }
<script>
</head>
<body>
<form:form method="post" name="fileViewForm"
    commandName="productionManager" action="addComment.htm" enctype="multipart/form-data" >
<table border="1" width="100%">
<tr>
<td rowspan="2">Client Name</td>
<td rowspan="2">App Name</td>
<td rowspan="2">Start Date</td>
<td rowspan="1" colspan="3" align="center">Phase</td>
<td rowspan="2">Target Date</td>
<td rowspan="2">Revenue Month</td>
<td rowspan="2">Priority</td>
</tr>
<tr>
<td>Phase</td>
<td>Status</td>
<td>Next Phase</td>
</tr>
<tr>
<td>test</td>
<td>test</td>
<td>test</td>
<td>current phase</td>
<td>

</td>
<td>test</td>
<td></td>
<td>Click to upload</td>
<td>

</td>
</tr>
</table>
    <table border="1" background="green" width="100%">
        <tr>
            <th>Comments/Message</th>
        </tr>
        <tr>
            <td colspan="3"><form:textarea path="model.newCommentsDTO.commentMessage" />
            </td>
        </tr>
        <tr>
            <td>
            <c:forEach var="fileDTO" items="${productionManager.model.newCommentsDTO.fileDTOList}">
                <input id="file" type="file" name="${fileDTO.file}" size="30"/>
            </c:forEach>
            <input type="submit" value="More" name="_addFileElement"/>
            </td>

        </tr>
        <tr>
            <td><input type="submit" name="_finish" value="Submit" /></td>
        </tr>

        <tr>
            <td>S.No</td>
            <td>Comments/Message</td>
            <td>Posted By</td>
            <td>Posted On</td>
        </tr>
        <c:set var="count" value="0"/>
        <c:forEach var="prodManagerDTO" items="${productionManager.models}">
            <c:forEach var="comments" items="${prodManagerDTO.commentsDTOList}"
                varStatus="status">
                <c:set var="count" value="${count +1}" />
            </c:forEach>
            <c:forEach var="comments" items="${prodManagerDTO.commentsDTOList}"
                varStatus="status" begin="1">
                <tr>
                    <td><c:out value="${status.count}" /></td>
                    <td><b><c:out value="${comments.commentMessage}" /></b> <br />
                    <table border="0">
                        <c:forEach var="files" items="${comments.fileDTOList}">
                            <tr>
                                <c:choose>
                                    <c:when test="${empty files.deletedDesc }">
                                        <c:if test="${!empty files.fileName}">
                                        <td><a
                                            href="../download.htm?file=<c:out value='${files.fileLocation}'/>&fileName=<c:out value='${files.fileName}'/>" /><c:out
                                            value="${files.fileName}" /></a></td>
                                        <td><a href="../deleteFiles.htm"
                                            onclick="setFileParameters('<c:out value="${files.fileLocation}"/>','<c:out value="${files.fileID}"/>','<c:out value="${files.fileName}"/>','deleteFile');">Delete
                                        Me!</a></td>
                                        </c:if>
                                    </c:when>
                                    <c:otherwise>
                                        <td><c:out value="${files.deletedDesc}"></c:out></td>
                                    </c:otherwise>
                                </c:choose>
                            </tr>
                        </c:forEach>
                    </table>
                    </td>
                    <td><c:out value="${comments.postedBy}" /></td>
                    <td><c:out value="${comments.postedOn}" /></td>
                </tr>
            </c:forEach>
        </c:forEach>
    </table>
    <form:input id="fileName" path="model.selectedFileDTO.fileName" cssStyle="display:none"/></td>
    <form:input id="fileID" path="model.selectedFileDTO.fileID" cssStyle="display:none"/></td>
    <form:input id="fileLocation" path="model.selectedFileDTO.fileLocation" cssStyle="display:none" /></td>
    <div id="delFileButtonDiv" style="display: none"><input
        type="submit" name="_deleteFiles" value="Delete File!" /><c:out
        value="${files.fileName}" /></div>
</form:form>
</body>
</html>


As you can see the statements in the jsp page highlighted in red color are responsible fro triggering the submit action. These submit action will trigger the corresponding methods in the CommentsController.java class.
For example if you click the submit button with name '_finish' then from the dispatcher servlet the method name will be resolved as 'finalSubmit'.

See the entry <prop key="_finish">finalSubmit</prop>.
The finalSubmit method will get the control from the jsp. Now as the controller is of type SimpleFormController, you have the access of the form elements as well and as the name of buttons are mapped to different methods of controller via the methodNameResolver and the dispatcher servlet different methods will be triggered accordingly.

Thanks for the posts at: http://forum.springsource.org/showthread.php?t=11702


Thanks.

1 comment:

  1. this approach works well in firefox for button click and form submissions as well. But in IE, it may require the button to be compulsorily clicked,so we have to manage the approach so that user clicks the buttons rather than make the button click by javascript.

    ReplyDelete