Page 1 of 5 123 ... LastLast
Results 1 to 10 of 47

Thread: Handling many to one relationships using a Form Controller

  1. #1
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    13

    Default Handling many to one relationships using a Form Controller

    Hello,

    I am currently working on a web form to edit a User object from my domain model. The User can be a member of zero or more Organisations and have zero or more Capabilities. I have decided to "spike" a Form Controller that extends the very handy looking SimpleFormController.

    Here is what I have so far..

    Code:
    public class UserFormController extends SimpleFormController {
    
        private UserDirectoryService uds;
        private OrganisationService os;
        private AuditService as;
    
        private static final char USER_FORM_JOIN_ORGANISATION='j';
        private static final char USER_FORM_LEAVE_ORGANISATION='l';
        private static final char USER_FORM_CHANGE_PASSWORD='p';
        private static final char USER_FORM_ADD_CAPABILITY='c';
        private static final char USER_FORM_DROP_CAPABILITY='d';
        private static final char USER_FORM_COMMAND_SUBMIT='s';
        
        public UserFormController() {
            setSessionForm(true);
            setValidateOnBinding(false);
            setCommandName("userForm");
            setFormView("editUserForm");
            setSuccessView("editUserForm");
        }
    
        public void setUserDirectoryService(UserDirectoryService uds) {
            this.uds = uds;
        }
        public void setOrganisationService(OrganisationService os) {
            this.os = os;
        }
        public void setAuditService(AuditService as) {
            this.as = as;
        }
    
        protected Object formBackingObject(HttpServletRequest request)
                throws Exception {
            String sId = request.getParameter("id");
            Long id = new Long(sId);
            if (id != null) {
                User u = uds.getUser(id);
                return new UserForm(uds.getUser(id), os.getAllOrgCodesAndDescriptions());
            }
            return new UserForm(os.getAllOrgCodesAndDescriptions());
        }
         
        protected ModelAndView onSubmit(HttpServletRequest request,
                HttpServletResponse response, Object command, BindException errors)
                throws Exception {
            
            UserForm uf = (UserForm) command;
            
            switch(request.getParameter("command").charAt(0)) {
            	case USER_FORM_JOIN_ORGANISATION:
            	    uf.getUser().addOrganisation(os.getOrganisation(uf.getAddedOrgCode()));
            	    break;
            	case USER_FORM_LEAVE_ORGANISATION:
            	    //uf.getUser().removeOrganisation(os.getOrganisation(uf.getAddedOrgCode()));
            	    break;
            	case USER_FORM_CHANGE_PASSWORD:
            	    uf.setChangePassword(true);
            	    break;
            	case USER_FORM_ADD_CAPABILITY:
            	    if (uf.isRoleOnlyCapability()) {
            	        uf.getUser().addCapability(uf.getAddedCapRole());
            	    }
            	    else {
            	        uf.getUser().addCapability(uf.getAddedCapRole(),
            	                os.getOrganisation(uf.getAddedCapOrgCode()),
            	                as.getMetaAudit(uf.getAddedCapAuditRef()));
            	    }
            	    break;
            	case USER_FORM_DROP_CAPABILITY:
            	    break;
            	case USER_FORM_COMMAND_SUBMIT:
            	    if (uf.isNewUser()) {
                        uds.saveUser(uf.getUser());
                    }
                    else {
                        uds.updateUser(uf.getUser());
                    }
            	    break;
            }
            return super.onSubmit(request,response,command,errors);
        }
    }
    The behavior I am after is to allow the administrator to manipulate the user but not to commit any changes until the form is submitted. As you can see, the form has multiple submit buttons for adding and dropping capabilities and organisations. I rather like the idea of the setSessionForm attribute although it will not work in this case as I believe that the simpleFormController (and even AbstractFormController) only expects a single submit button (and therefore success or failiure).

    Has anybody managed to find an elegant solution/example to this...

    Cheers,


    James

    PS please ignore the obvious ommisions (binding, validation, etc).
    PPS using velocity.

  2. #2
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    71

    Default

    I think you would be better off using an extension of AbstractWizardFormController and a sessionForm=true of course.

    You could have a main page which binds the form elements to the main attributes of the user object with seperate pages for adding capabilities/organisations. This will allow you to use the
    showPage() methods to create a new Organisation object, after submission you can then validate the object in validatePage() and add it to the Set of your organisations in the original form object.

    By using submit buttons with names "_targetX" where X is the number of the page to go to you can navigate to the addOrganisation addCapability screens

    ie,
    page 1 = main
    page 2 = addOrganisation
    page 3 = removeOrganisation
    page 4 = addCapability
    page 5 = removeCapability

    on the main screen there will be a submit button "_finish" which will allow you to use processFinish() method to persist the model.

    I havn't tried this myself so can't be sure if it'll work/elegant efficent solution but it may give u ideas.

  3. #3
    Join Date
    Aug 2004
    Location
    Carlisle, UK
    Posts
    184

    Default

    I have a number of forms doing what you suggest, with mutiple submit buttons, and different behaviour depending on the button clicked.

    I think you may be limiting your flexibility by terminating with
    Code:
    return super.onSubmit(request,response,command,errors);
    If, instead, you finish with
    Code:
    return new ModelAndView(view, model);
    you can construct a model, and return to a view, which relates to the button pressed.
    If you want more than just the formView and successView which SimpleFormController gives you, you can define them as bean properties in your controller class and configure them in the Spring configuration of the controller.
    Chris Harris
    Carlisle, UK

  4. #4
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    13

    Default

    Hi Stu,

    Firstly, well done zeroing in on the problem, I do indeed want to take advantage of Spring's binding utilities as well as maintaining the form object in memory until it is time to save the User.

    I have a sneaking suspicion that the AbstractWizardForm Controller is the tidiest solution in terms of functionality. However, the user experience I am trying to get at involves having a one-stop form where administrators can easily manipulate this User object.

    Here is the velocity template:
    Code:
    <html>
    <body>
    	<h3>Edit User</h3>
    	<form action="editUser.do" method="POST">
    		Name&#58; #springFormInput&#40;"userForm.user.userName" ""&#41;<br>
    		email&#58; #springFormInput&#40;"userForm.user.email" ""&#41;<br>
    		#if &#40;$&#123;userForm.changePassword&#125;&#41;
    			Password&#58; #springBind&#40;"userForm.user.password"&#41;
    				<input type="password" name="$&#123;status.expression&#125;" value="$!&#123;status.value&#125;" /><br>
    			Confirm&#58; #springBind&#40;"userForm.repeatedPassword"&#41;
    				<input type="password" name="$&#123;status.expression&#125;" value="$!&#123;status.value&#125;" /><br>
    		#else
    			<button name="command" value="p">Change Password</button>
    		#end
    		<h4>Organisations</h4>
    		<p>This user is a member of the following organisations</p>
    		#if&#40; $&#123;userForm.user.organisations.size&#40;&#41;&#125; > 0 &#41;
    			#set&#40;$dropCode = "___org" &#41;
    			<table border="1" summary="organisation membership">
    				<thead>
    					<tr>
    					<th>Code</th><th>Description</th><th></th>
    					</tr>
    				</thead>
    				<tbody>
    					#foreach &#40;$org in $&#123;userForm.user.organisations&#125;&#41; 
    						<tr>
    							<td>$&#123;org.code&#125;</td><td>$&#123;org.description&#125;</td><td><input type="checkbox" name="$dropCode$&#123;org.code&#125;"/></td>
    						</tr>
    					#end 
    				</tbody>
    			</table>
    			<button name="command" value="l">Drop selected organisations</button>
    		#else
    			<p>No memberships</p>
    		#end
    		
    		Join an organisation&#58; #springFormSingleSelect&#40;"userForm.addedOrgCode" $&#123;userForm.orgList&#125; ""&#41;<button name="command" value="j">Join</button>
    		
    		<h4>Capabilities</h4>
    		<p>This user enjoys the following capabilities</p>
    		#if&#40; $&#123;userForm.user.capabilities.size&#40;&#41;&#125; > 0 &#41;
    			#set&#40;$dropCode = "___cap" &#41;
    			<table border="1" summary="capabilities">
    				<thead>
    					<tr>
    					<th>Role</th><th>Organisation<i>&#40;optional&#41;</i></th><th>Audit<i>&#40;optional&#41;</i></th>
    					</tr>
    				</thead>
    				<tbody>
    					#foreach &#40;$cap in $&#123;userForm.user.capabilities&#125;&#41; 
    						<tr>
    							<td>$&#123;cap.role&#125;</td><td>$!&#123;cap.org.description&#125;</td>
    							<td>$!&#123;cap.audit.auditRef&#125;</td><td><input type="checkbox" name="$dropCode$&#123;cap.id&#125;"/></td>
    						</tr>
    					#end 
    				</tbody>
    			</table>
    			<button name="command" value="d">Drop selected capablilities</button>
    		#else
    			<p>No capabilities</p>
    		#end
    	</form>
    </body>
    </html>
    As far as I can tell, I'm going to either:

    a). Write a new controller (extending the BaseCommandController as we can not override the getCommand()) and pop the backing form object into the session.
    b). Extend the SimpleFormController, set sessionForm = false and use hidden fields to manage the many to one lists (yuck - how messy)
    c). Extend the SimpleFormController, set sessionForm = false and then ask the formBackingObject to use WebUtils.getOrCreateSessionAttribute().
    d). Abuse the AbstractWizardFormController by setting all the pages to return the main one.
    e). In the case statements, for the cases where we are updating the userForm but not writing the User away, push the userform back into the session.

    It's a shame because the SimpleFormController almost works - it just keeps removing the session object as soon as it's retrieved it (which is great for simple forms - I know).

    Sod's law says I'm probably going to do none of these, but I will post back my eventual solution.

    In the meantime any advice is most welcome.


    Cheers,


    James

  5. #5
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    13

    Default

    Hi Chris,

    Thanks for you comment, I shall concentrate more on what is coming out of the onSubmit method.

    Cheers,

    James

  6. #6
    Join Date
    Oct 2004
    Location
    NYC, USA
    Posts
    13

    Default

    Quote Originally Posted by cmgharris
    I have a number of forms doing what you suggest, with mutiple submit buttons, and different behaviour depending on the button clicked....
    If, instead, you finish with
    Code:
    return new ModelAndView&#40;view, model&#41;;
    you can construct a model, and return to a view, which relates to the button pressed....
    I hate to threadjack your thread James, but I'm trying to do a similar thing and this suggestion really interests me. Chris, if you could give a bit more info on what's involved in this?

    Thanks,
    Pete

  7. #7
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    13

    Default

    No worries Pete, jack away.

    btw, am now making some progress (by using option e) with this as I can now add and remove organisations to a user without persisting it. I shall keep this thread 'posted' when the tests are written.
    James Gellately-Smith

  8. #8
    Join Date
    Aug 2004
    Location
    Carlisle, UK
    Posts
    184

    Default

    if you could give a bit more info on what's involved in this?
    Well, if you exit your onSubmit method with
    Code:
    return super.onSubmit&#40;request,response,command,errors&#41;;
    then as far as I can see, you'll get the default ModelAndView consisting of the successView as the view, and errors.getModel() as the model (i.e. your populated form backing object, and any errors).

    If you create your own ModelAndView, you have complete control over the model and the view returned. You'll often want to use getSuccessView() for the view, but you don't have to. And if you use the same view for formView and successView (as I often do), you'll probably want to initialise your model with
    Code:
    Map model = errors.getModel&#40;&#41;;
    to make sure you've got the errors instance with the form backing object in the model.
    Then you can add additional model elements as your view requires.

    Hope that helps
    Chris Harris
    Carlisle, UK

  9. #9
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    71

    Default

    I would suggest using the MultiActionController which does have support for binding request params to Command objects if the delegate methods have a command object parameter (this appears to be an undocumented feature but a nice one!)

    You would have to create a new resolver to deal with the multi-submit buttons. i would suggest this:

    Code:
    public class SubmitParameterPropertiesMethodNameResolver implements
    		MethodNameResolver &#123;
    
    	private Properties mappings;
    
    	/**
    	 * 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&#40;Properties mappings&#41; &#123;
    		this.mappings = mappings;
    	&#125;
    	
    	public void afterPropertiesSet&#40;&#41; &#123;
    		if &#40;this.mappings == null || this.mappings.isEmpty&#40;&#41;&#41; &#123;
    			throw new IllegalArgumentException&#40;"'mappings' property is required"&#41;;
    		&#125;
    	&#125;
    
    	public String getHandlerMethodName&#40;HttpServletRequest request&#41;
    			throws NoSuchRequestHandlingMethodException &#123;
    	
    		for &#40;Iterator it = this.mappings.keySet&#40;&#41;.iterator&#40;&#41;; it.hasNext&#40;&#41;;&#41; &#123;
    			String submitParamter = &#40;String&#41; it.next&#40;&#41;;
    			if &#40;WebUtils.hasSubmitParameter&#40;request, submitParamter&#41;&#41; &#123;
    				return &#40;String&#41; this.mappings.get&#40;submitParamter&#41;;
    			&#125;
    		&#125;
    		return null;
    	&#125;
    which would map from a properties file of _addOrganisation=methodName
    where the button name="_addOrganisation" (or "_addOrganisation.x) for image buttons.

    But the multi-action does binds but doesnt do errors natively.
    whats really needed is a MultiActionFormController

    which at its simplest could be a little like this
    Code:
    public abstract class SimpleMultiActionFormController extends SimpleFormController &#123;
    	
    	private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver&#40;&#41;;
    	
    	public final void setMethodNameResolver&#40;MethodNameResolver methodNameResolver&#41; &#123;
    		this.methodNameResolver = methodNameResolver;
    	&#125;
    	
    	public final MethodNameResolver getMethodNameResolver&#40;&#41; &#123;
    		return this.methodNameResolver;
    	&#125;
    	
    	protected ModelAndView processFormSubmission&#40;HttpServletRequest request,
    			HttpServletResponse response, Object command, BindException errors&#41;
    			throws Exception &#123;
    		
    		if &#40;errors.hasErrors&#40;&#41; || isFormChangeRequest&#40;request&#41;&#41; &#123;
    			if &#40;logger.isDebugEnabled&#40;&#41;&#41; &#123;
    				logger.debug&#40;"Data binding errors&#58; " + errors.getErrorCount&#40;&#41;&#41;;
    			&#125;
    			return showForm&#40;request, response, errors&#41;;
    		&#125;
    		else &#123;
    			String methodName = this.methodNameResolver.getHandlerMethodName&#40;request&#41;;
    			
    			Method m = &#40;Method&#41; this.getClass&#40;&#41;.getMethod&#40;methodName,
    					new Class&#91;&#93; &#123;Object.class,
    								 BindException.class&#125;&#41;;
    			if &#40;m == null&#41; &#123;
    				throw new NoSuchRequestHandlingMethodException&#40;methodName, getClass&#40;&#41;&#41;;
    			&#125;
    
    			
    			List params = new ArrayList&#40;2&#41;;
    			params.add&#40;command&#41;;
    			params.add&#40;errors&#41;;
    			
    			return &#40;ModelAndView&#41; m.invoke&#40;this, params.toArray&#40;new Object&#91;params.size&#40;&#41;&#93;&#41;&#41;;
    		&#125;
    		
    	&#125;
    
    &#125;
    which using the above methodname resolver would resolve to subclass methods such as
    addOrganisation(Object command, BindException errors) throws Exception
    addCapability(Object command, BindException errors) throws Exception
    finalSubmit(Object command, BindException errors) throws Exception

    hope this helps

    DISCLAIMER: no idea if this code works as i havnt tested it. -Stuart

  10. #10
    Join Date
    Oct 2004
    Location
    London, UK
    Posts
    13

    Default

    Stuart,

    Thanks, what an elegant solution, we (pair programming) do now have a solution but I'm going to refactor it to extend your SimpleMultiActionFormController. The main issue for us was that the backing form kept getting dropped out of the session and the references were not regenerated. We solved this by whacking the session back in:
    Code:
        // with the following line, we are just being consistent with the 
        // AbstractFormCOntroller.showForm method. Sending the command object
        // would be just as good.
        request.getSession&#40;&#41;.setAttribute&#40;getFormSessionAttributeName&#40;&#41;,errors.getTarget&#40;&#41;&#41;;
    And by regenerating the references:
    Code:
        // errors.getModel returns a duplicate each time and
        // therefore should only be called once.
        Map model = errors.getModel&#40;&#41;;
        model.putAll&#40;getReferenceData&#40;&#41;&#41;;
    For those instances where we were not submitting the form.

    I'll post it all when I'm done.
    James Gellately-Smith

Similar Threads

  1. Replies: 4
    Last Post: Sep 1st, 2010, 01:38 AM
  2. Replies: 3
    Last Post: Jun 8th, 2010, 03:27 AM
  3. Replies: 6
    Last Post: Jul 20th, 2007, 05:56 AM
  4. Replies: 0
    Last Post: Jun 10th, 2005, 08:22 AM
  5. Controller to Form Navigation?
    By cnelson in forum Web
    Replies: 1
    Last Post: Aug 20th, 2004, 06:18 AM

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •