Thursday, March 7, 2013

Using Finders In Roo

The Spring Roo shell provides commands to create finder methods for entity classes.  Where finders are Roo maintained static methods wrapping JPQL queries to retrieve subsets of application entities.  Additionally as part of Spring Roo's web tier JSP implementation a filter tag is generated.  However neither the tags usage or how to use this tag with finders is in the documentation.

The out of the box implementation generated by Spring Roo provides basic listing functionality.  However often times in real life applications lists of some entities can be rather lengthy making it difficult for users to find particular entities without having to traverse multiple pages.  Applying a finder method allows the user to narrow the scope of entities being shown and subsequently finding the entity(s) of interest easier to find.

So lets first get basic filtering up and running in a web application. Setting up an using a finder involves multiple steps;
  • First we will need to create the entity finder using the Roo shell.
  • Then a new filter method to access the finder will need to be added to the controller.
  • Modify the list JSP to render the filter.
The example here will focus on the entity named AppUser.  This entity represents users in an application.  Our filter is going to address the use case where a user wants to search for other users.  The AppUser entity has two fields, name and email address, that we are going to filter on.  The user will be able to enter in a single string and the application will display users where either the name the name or email address contains that string.

First we will create the finder using the Roo shell with the command:

finder add findAppUsersByNameLikeOrEmailLike


This results in Roo generating the following code;

    public static TypedQuery AppUser.findAppUsersByNameLikeOrEmailLike(String name, String email) {
        if (name == null || name.length() == 0) throw new IllegalArgumentException("The name argument is required");
        name = name.replace('*', '%');
        if (name.charAt(0) != '%') {
            name = "%" + name;
        }
        if (name.charAt(name.length() - 1) != '%') {
            name = name + "%";
        }
        if (email == null || email.length() == 0) throw new IllegalArgumentException("The email argument is required");
        email = email.replace('*', '%');
        if (email.charAt(0) != '%') {
            email = "%" + email;
        }
        if (email.charAt(email.length() - 1) != '%') {
            email = email + "%";
        }
        EntityManager em = AppUser.entityManager();
        TypedQuery q = em.createQuery("SELECT o FROM AppUser AS o WHERE LOWER(o.name) LIKE LOWER(:name)  OR LOWER(o.email) LIKE LOWER(:email)", AppUser.class);
        q.setParameter("name", name);
        q.setParameter("email", email);
        return q;
    }

Note that Roo has done all the work of preparing the like parameters for us, substituting the '*' with '%' and placing a '%' on each end of the parameter.  Thus when a parameter such as, 'foo*bar' is supplied to the finder method the parameter provided to the query will be '%foo%bar%'.

In the JSP we will be using the find tag (tags/form/find.tagx) this tag embeds a form containing the filter parameters on that page.  Within the form is a hidden field named 'finder' that contains a symbolic name of the finder we want to use.   The Roo generated web application does not contain any special logic to auto-magically interpret the finder name and invoke that finder without having to provide hand written logic.  That logic is what we will have to provide in the controller method.  The finder name allows a controller method to support access to multiple finders.  We will not be using that functionality here but will still include the logic to test the name as a defense against wayward JSP errors else where in the application.

Finally, note that the return type is a JPA TypedQuery object.  Compare this to the findAllAppUsers method in the AppUser_Roo_Jpa_ActiveRecord.aj file, that returns a list of AppUsers.  The list JSP (/tags/form/list.tagx) expects a list of entities, so the controller method will have to invoke getResultList on the results of the entity finder and place that list in the uiModel.

Now that the finder is available we can now code the controller method.

package com.repik.multitenant.security.web;

import com.repik.multitenant.security.domain.AppUser;
import org.springframework.roo.addon.web.mvc.controller.scaffold.RooWebScaffold;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@RequestMapping("/appusers")
@Controller
@RooWebScaffold(path = "appusers", formBackingObject = AppUser.class)
public class AppUserController {

   @RequestMapping(value="/find", produces = "text/html")
    public String find(
         @RequestParam(value = "find", required = true) String finder,
         @RequestParam(value = "filter", required = true) String filter,  Model uiModel) {

      // test that the finder parameter is correct
      if ( "findAppUsersByNameLikeOrEmailLike".equals( finder ) ) {

         // notice that getResultList() is invoked on the finder result
         uiModel.addAttribute("appusers", AppUser.findAppUsersByNameLikeOrEmailLike(filter, filter).getResultList()) ;

         uiModel.addAttribute("filter", filter) ;
         return "appusers/list";
      }
      else
         return "redirect:/appusers" ;
    }
}

Finally we need to add the find tag to the AppUser list JSP (views/appuser/list.jspx) to generate the HTML that invokes the controller method that we just created.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<div xmlns:jsp="http://java.sun.com/JSP/Page" 
  xmlns:page="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"/>
    
 <page:find finderName="findAppUsersByNameLikeOrEmailLike" id="ff_com_repik_multitenant_security_domain_appuser" path="/appusers/find">
  <input data-dojo-props="trim:true" data-dojo-type="dijit/form/TextBox" id="toolbarFilter" name="filter" type="text" value="${filter}"/>
 </page:find>
 
 <page:list id="pl_com_repik_multitenant_security_domain_AppUser" items="${appusers}" z="HqueBBVm/RaI2SUiUNBFWmNCOZ0=">
  <table:table create="false" data="${appusers}" delete="false" id="l_com_repik_multitenant_security_domain_AppUser" path="/appusers" update="false" multiselect="true" z="user-managed">
   <table:column id="c_com_repik_multitenant_security_domain_AppUser_name" property="name" z="hitrYtAgDxnq3qbzqZWR5cdLT4o="/>
   <table:column id="c_com_repik_multitenant_security_domain_AppUser_email" property="email" z="gzkRNxtd6O1mO8woYZje/UYflcg="/>
  </table:table>
 </page:list>
</div>


The JSP we added was the page:find tag.  The finderName attribute value must be 'findAppUsersByNameLikeOrEmailLike' that is the value being tested in the controller method.  Within the tag itself is a input text element that the user can enter the filter text into.  Notice that a data-dojo-props attribute has been added with the value of "trim:true" and the element has been identified as a text box to Dojo using the data-dojo-type attribute.  This allows Dojo to trim the filter text for us.

 <page:find finderName="findAppUsersByNameLikeOrEmailLike" id="ff_com_repik_multitenant_security_domain_AppUser_finder" path="/appusers/find">
  <input data-dojo-props="trim:true" data-dojo-type="dijit/form/TextBox" id="toolbarFilter" name="filter" type="text" value="${filter}"/>
 </page:find>

We now completed using a finder in Spring Roo.

1 comment:

  1. How would you handle a finder searching on string and entity? The filter needs to be a string and then cast to the entity for the method call in the controller. Would you be able to enter the search using just one input?

    ReplyDelete