Thursday, July 25, 2013

Handling Multi-Selection

In an earlier post Restful URLs for Managing Entity Relationships, I developed the set of restful URL's needed to manage associative relationships between independent entities.  Now in this post it time to implement this design.

In that post we discovered that to support a one to many relationship between two entities the following use case scenarios need to be supported;
  • Show the list of entities associated with an entity
  • Add form to allow the user to select entities for association with the entity.
  • Add associations between an entity and those that the user selected.
  • Remove form allowing the user to select entities associated with an entity for removal.
  • Remove associations between an entity and those that the user selected for removal.
Also in other posts I have noted that adding functionality to Roo applications using a JPA/JSP technology stack involves the following tasks;
  • Building the entity queries needed,
  • Adding the controller methods, 
  • Adding the supporting JSP's.
With that information, scoping out the tasks to implement this can be easily done.

Building the Entity Manipulation Methods

We will need to build four entity manipulation methods to cover the five operations, two queries and two entity modification methods.  They are;
  • Create an association with another entity.
  • Remove an association with another entity.
  • Return the collection of entities that are associated with the entity..
  • Return the collection of entities that are not associated with the entity.
The method to select the associated entities can be used for both the show and add form operations.  The last query will be an expensive query to execute, so controller implementations use this method judiciously.

It's important to realize that through the course of development, the scope of implementation is only one end of one relationship.  In complete applications many such relationships should be expected,  Subsequently one should conclude that code similar to the implementation here will be scattered throughout the domain code base.  Additionally a domain entity can maintain multiple relationships resulting in multiple manifestations of the code within the bounds of an entity. So we need to be methodical in organizing this code.

One final take away when considering the volume of similar code involved here is that we are having to manage multiple manifestations of similar code.  That's what Roo is really good it, so long term it's going to be in our interest to have an add on do this work.  And that's precisely where we are ultimately headed, but we are getting a little ahead of ourselves here.  Still, we need structure the code to be add on implementation friendly.

We will put all these methods in the same ITD and name it '<entity>_Association'. Here I am copying Roo's example and putting all this functionality in a separate ITD.  Here the '<entity>' represents the entity for one end of the association.

The subsequent implementation utilizes typical JPA functionality, with the most thought intensive part was query construction.  The query methods return a TypedQuery object the same as the finders that Roo constructs.  Some care in naming of the methods is required because functionality expressing more than one relationship can occur in the entities class method name namespace.

And finally the biggest gotcha that I stumbled across while implementing was that entity modification cannot be done in static code.  The finder methods are static, which is reasonable since we don't have and entity object to start with.  So it's tempting to want to do the same with the create and delete methods.  Particularity since here we are starting out with two entity id's for the operation.  After putting our RDBMS cap on, the delete method is a simple delete from where statement. allowing us to avoid having to get the entity.  JPA doesn't like that approach, instead one has to retrieve the entity object and then create and delete associations with that object.

Here is the final implementation for the entity methods;

package com.repik.multitenant.security.domain;

import com.repik.multitenant.security.domain.AppUser;
import javax.persistence.EntityManager;
import javax.persistence.TypedQuery;

public privileged aspect AppUser_Association {

 public static TypedQuery AppUser.findOrganizationsAssociatedWithAppUser(Long userId) {
  if (userId == null)
   throw new IllegalArgumentException("The userId argument is required");
  
  EntityManager em = AppUser.entityManager();
  TypedQuery q = em.createQuery(
    "SELECT ou.organization FROM OrganizationUser ou WHERE ou.appUser.id = :userId",
    Organization.class);
  q.setParameter("userId", userId);
  return q;
 }

 AppUser.findOrganizationsNotAssociatedWithAppUser(Long userId) {
  if (userId == null)
   throw new IllegalArgumentException("The userId argument is required");
  
  EntityManager em = AppUser.entityManager();
  TypedQuery q = em.createQuery(
    "SELECT o FROM Organization o WHERE o.id not in ( "
        + "SELECT ou.organization.id FROM OrganizationUser ou WHERE ou.appUser.id = :userId )",
    Organization.class);
  q.setParameter("userId", userId);
  return q;
 }

  public void AppUser.createOrganizationUser(Long organizationId ) {
    Organization organization = Organization.findOrganization( organizationId ) ; 
      if (organization == null) throw new IllegalArgumentException("The organization argument is required");

      OrganizationUser ou = new OrganizationUser() ;
      ou.setOrganization( organization ) ;
      ou.setAppUser( this ) ;
      ou.persist() ;
    }

  public Integer AppUser.deleteOrganizationUser(Long organizationId ) {
    if (organizationId == null) throw new IllegalArgumentException("The organizationId argument is required");

    EntityManager em = OrganizationUser.entityManager();
    Query q = em.createQuery("DELETE FROM OrganizationUser ou WHERE ou.organization.id = :organizationId AND ou.appUser.id = :userId" ) ;
    q.setParameter("organizationId", organizationId);
    q.setParameter("userId", id );
    try {
      return q.executeUpdate();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return 0 ;
    }
}


Adding the Controller Methods

Now that the entity methods are in place we are ready to work on the presentation side of the application. Starting with the controller methods.  We have the service mappings that a required and now the entity methods the implementation can be completed.

package com.repik.multitenant.security.web;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.http.HttpRequest;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import com.repik.multitenant.security.domain.AppUser;
import com.repik.multitenant.security.domain.Organization;
import com.repik.multitenant.security.domain.OrganizationUser;

privileged aspect AppUserController_OrganizationAssociations {

 @RequestMapping(value="/{userId}/organizations", produces = "text/html")
    public String AppUserController.showOrganizations(
      @PathVariable( "userId" ) Long userId, Model uiModel ) {
        uiModel.addAttribute("organizations", AppUser.findOrganizationsAssociatedWithAppUser(userId).getResultList());
        uiModel.addAttribute("userId", userId);
        return "appusers/organizations/show";
 }
 
 @RequestMapping(value="/{userId}/organizations", params="form", produces = "text/html")
    public String AppUserController.addOrganizationsForm(
      @PathVariable( "userId" ) Long userId,
      @RequestParam( value="form", required=true ) String form, 
      Model uiModel ) {
  if ( "add".equals( form )) {
   uiModel.addAttribute("organizations", AppUser.findOrganizationsNotAssociatedWithAppUser(userId).getResultList());
   uiModel.addAttribute( "method", "POST" ) ;
  }
  else {
         uiModel.addAttribute("organizations", AppUser.findOrganizationsAssociatedWithAppUser(userId).getResultList());
   uiModel.addAttribute( "method", "DELETE" ) ;
  }
        uiModel.addAttribute("userId", userId);
        return "appusers/organizations/edit";
 }
 
 @RequestMapping(value="/{userId}/organizations", method = RequestMethod.POST, produces = "text/html")
 @Transactional
    public String AppUserController.addOrganizations( HttpServletRequest request,
      @PathVariable( "userId" ) Long userId, Model uiModel ) {
  AppUser user = AppUser.findAppUser(userId) ;
        if (user == null) throw new IllegalArgumentException("User not found");

  String[] organizationIds = request.getParameterValues( "organizationId" ) ;
  for ( int i = 0 ; i < organizationIds.length ; i++ ) {
   Long organizationId = Long.parseLong( organizationIds[ i ] ) ;
   user.createOrganizationUser(organizationId) ;
  }

        return "redirect:/appusers/" + userId + "/organizations";

 }
 
 @RequestMapping(value="/{userId}/organizations", method = RequestMethod.DELETE, produces = "text/html")
 @Transactional
    public String AppUserController.deleteOrganizations( HttpServletRequest request, 
      @PathVariable( "userId" ) Long userId, Model uiModel ) {
        AppUser user = AppUser.findAppUser(userId) ;
        if (user == null) throw new IllegalArgumentException("User not found");

  String[] organizationIds = request.getParameterValues( "organizationId" ) ;
  for ( int i = 0 ; i < organizationIds.length ; i++ ) {
   Long organizationId = Long.parseLong( organizationIds[ i ] ) ;
   user.deleteOrganizationUser(organizationId) ;
  }

        return "redirect:/appusers/" + userId + "/organizations";
 }
}


Adding the JSP's

The final step is to build the JSP to display the lists of entities that are either associated or not with the entities.  We also need to display these lists in either show or update mode, where the difference between the two is the appearance of a check box.

While it's tempting to think that we could use just one JSP template to do it all.  But adding check boxes means we also have to put them in a form.  That means we have to build and keep track form elements and URL's.  So it's best to keep viewing and editing as separate JSP's.

We will need to modify the views.xml and add the two JSP templates.  Recall that in the webapp that Roo setup all of the tiles and JSP files for the entities are located in; '/src/main/webapp/WEB-INF/views'.  In that folder each entity has a folder, so the focus of the modifications we will be making will be to the 'organizations' folder.

Defining the Tiles Views

In the view.xml found in the 'organizations' folder two tiles definitions need to be added to show and update multiple selections.

 <definition extends="default" name="appusers/organizations/show">
    <put-attribute name="body" value="/WEB-INF/views/appusers/showOrganizations.jspx" />
</definition>

<definition extends="default" name="appusers/organizations/edit">
    <put-attribute name="body" value="/WEB-INF/views/appusers/updateOrganizations.jspx" />
</definition>

Show Organizations

Next up is the show organizations JSP (showOrganizations.jspx).  The structure of this file is similar to the list JSP file for the organizations entity that Roo generated.  The differences between the two JSP's is that the ability to create, update, and delete organizations has been turned off.  Deactivating these operations is optional, it's done here because user should be focused on what organizations are associated with a particular user rather then performing organization management functions.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page" 
  xmlns:form="urn:jsptagdir:/WEB-INF/tags/form" 
  xmlns:table="urn:jsptagdir:/WEB-INF/tags/form/fields" 
  version="2.0">
    <jsp:directive.page contentType="text/html;charset=UTF-8"/>
    <jsp:output omit-xml-declaration="yes"/>
    <table:table data="${organizations}" id="l_com_repik_multitenant_security_domain_Organization" create="false" update="false" delete="false" path="/organizations" z="5U3sseAfezAakwxsdptI+SXks8o=">
        <table:column id="c_com_repik_multitenant_security_domain_Organization_name" property="name" z="8FaKf+BmDqidSHXdwfn1tV1TnQo="/>
        <table:column id="c_com_repik_multitenant_security_domain_Organization_parent" property="parent" z="D0HsRlG9onaVgrMyzSjE6hpa4NU="/>
        <table:column id="c_com_repik_multitenant_security_domain_Organization_roles" property="roles" z="1F8qMV7ifZp90bu0SUpI+uNwzyY="/>
        <table:column id="c_com_repik_multitenant_security_domain_Organization_description" property="description" z="2tWVqb2uDhtkEVsFS0M2anZ68vg="/>
    </table:table>
</div>

Update Organizations

The last part is the update organizations JSP (updateOrganizations.jspx).  This JSP is used for both creating and removing associations between AppUsers and Organizations.  There's a couple of things going on in this file, making it more complex then showOrganizations.jspx.

First of course is that the table showing the organizations is now a multiple selection table using the organizationId for the value of the checkboxes.  While deactivating the create, update and delete operations was a matter of preference before with the showOrganizations.jspx file, doing that here is important.  We want to prevent navigation away from this screen since we want the user focused on managing the associations between an AppUser and Organizations rather then managing the Organizations themselves.

Also the multiple selection table needs to be wrapped in a form so the results of the user selection can be sent to the server.  Adding the form requires; setting up the form itself and of course adding a submit button.


<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page" 
  xmlns:c="http://java.sun.com/jsp/jstl/core"
  xmlns:fn="http://java.sun.com/jsp/jstl/functions" 
  xmlns:form="http://www.springframework.org/tags/form" 
  xmlns:spring="http://www.springframework.org/tags"
  xmlns:table="urn:jsptagdir:/WEB-INF/tags/form/fields" 
  version="2.0">
    <jsp:directive.page contentType="text/html;charset=UTF-8"/>
    <jsp:output omit-xml-declaration="yes"/>
    <spring:url value="/appusers/${userId}/organizations" var="form_url" />

    <c:set var="enctype" value="application/x-www-form-urlencoded"/>
    <c:set var="idField" value="userId" />
    <c:set var="versionField" value="none" />

    <form:form action="${form_url}" method="${method}" modelAttribute="${modelAttribute}" enctype="${enctype}">
        <table:table data="${organizations}" id="l_com_repik_multitenant_security_domain_Organization" 
                path="/organizations" create="false" update="false" delete="false" 
                multiselect="organizationId" show="false" 
                z="5U3sseAfezAakwxsdptI+SXks8o=">
            <table:column id="c_com_repik_multitenant_security_domain_Organization_name" property="name" z="8FaKf+BmDqidSHXdwfn1tV1TnQo="/>
            <table:column id="c_com_repik_multitenant_security_domain_Organization_description" property="description" z="2tWVqb2uDhtkEVsFS0M2anZ68vg="/>
        </table:table>
        <div class="submit" id="${fn:escapeXml(userId)}_submit">
            <spring:message code="button_save" var="save_button" htmlEscape="false" />
            <script type="text/javascript">Spring.addDecoration(new Spring.ValidateAllDecoration({elementId:'proceed', event:'onclick'}));</script>
            <input id="proceed" type="submit" value="${fn:escapeXml(save_button)}" />
        </div>
    </form:form>
</div>


Now that the the entity queries, controller methods and JSP elements are in place multiple selection of elements between an Organization and AppUser is now functionality available.  What still is missing in incorporating the appropriate links into the menu system to access the new URL's.

This code supports one side of a many to many relationship, Organizations to AppUsers.  We will need to build similar code to handle the other direction of the association; AppUsers to Organizations.  Additionally, we will need to do the same for all the other associations appearing in our application entity model.  Building all this code is a lot of work.  But it's tasks like this that Roo excels at, all we need to do is build and add on that generates this code for us.  And that's precisely where we will go next.