Search code examples
javaspringspring-mvcspring-roo

Spring/roo custom finder


I want to create a custom finder (being a web development veteran but a complete first-timer at Java, Spring, AND Roo).

The first method below is from my "Asset" view controller class. It's what happens on your first hit to the asset administration page--it comes back with a view and a batch of the currently active assets. It works fine:

@RequestMapping("/assets")
public ModelAndView listAssets() {
    ModelAndView mav = new ModelAndView();
    mav.setViewName("admin/asset");
    Collection<com.myapp.model.Asset> assets = com.myapp.model.Asset.findAllAssets() ;
    mav.addObject("assets", assets);    
    return mav;
}

So now I want to have a "POST" request to that URL accept the submission of the asset search form, which contains fields for several of the key properties of the Asset object.

(Right now I'm coding that finder in my view controller, and I know ultimately I'll want to push that down to the model class. For now I'm just working it here for convenience sake. Also, I know I don't have to give the fully qualified name of the object, I've newbishly named my controller the same name as the model it's talking to, so until I sort that out, I'm working around it.)

I've borrowed some code from some Roo finders I generated, and ended up with:

@RequestMapping(value = "/assets", method = RequestMethod.POST)
public ModelAndView searchAssets(
            @RequestParam("category") String category,
            @RequestParam("year") String year,
            @RequestParam("manufacturer") String manufacturer,
            @RequestParam("drive_type") String driveType,
            @RequestParam("subcategory") String subcategory,
            @RequestParam("serial") String serial,
            @RequestParam("listing_type") String listingType,
            @RequestParam("hours") String hours,
            @RequestParam("model") String model,
            @RequestParam("mileage") String mileage  ) {
    ModelAndView mav = new ModelAndView();
    mav.setViewName("admin/asset");


    EntityManager em = com.myapp.model.Asset.entityManager();
    TypedQuery<Asset> q = em.createQuery("SELECT o FROM Asset AS o ", Asset.class);
    Collection<Asset> assets =  q.getResultList();
    mav.addObject("assets", assets);

    return mav;
}

So the questions are:

1) Is there a way to get a collection of my parameters, rather than baking them into the method signature? Because I sort of hate that.

2) Is there a way to iterate over my parameters and generate the WHERE clause of my query string that way? I'd like to not have to know much about the fields I'm being given here--and I need to be able to handle that I mostly won't have all of them. So I'd prefer to just build my query iteratively rather than declaratively, if that makes sense.

How would you go about building the select statement here?

EDIT (Solution):

@madth3 pointed me in the right direction here. Here's what I ended up with, which I'm pretty proud of:

public static TypedQuery<Asset> findAssetsByWebRequest( WebRequest request) {

    ArrayList<String> wheres = new ArrayList<String>();

    Iterator<String> i = request.getParameterNames();

    while (i.hasNext()) {
        String fieldname = i.next();
        String value = request.getParameter(fieldname);

        if (value.length() == 0) {
            continue;
        }

        if (fieldname.equals("manufacturer") || fieldname.equals("model") ) {

            value = value.replace('*', '%');
            if (value.charAt(0) != '%') {
                value = "%" + value;
            }
            if (value.charAt(value.length() - 1) != '%') {
                value = value + "%";
            }
            wheres.add(" o." + fieldname + " LIKE '" + value + "'");
        }
        else if (fieldname.contains("min")) {
            fieldname = fieldname.replace("min", "").toLowerCase();
            wheres.add(" o." + fieldname + " >= '" + value + "' ");
        }
        else if (fieldname.contains("max")) {
            fieldname = fieldname.replace("max", "").toLowerCase();
            wheres.add(" o." + fieldname + " <= '" + value + "' ");

        }
        else {
            wheres.add(" o." + fieldname + " = '" + value + "' ");
        }
    }


    String query = "SELECT o FROM Asset AS o ";
    if (wheres.size() > 0) {
        query += " WHERE ";
        for (String clause: wheres) {
            query += clause + " AND "; 
        }
        query += " 1 = 1 ";
    }


    EntityManager em = Asset.entityManager();
    TypedQuery<Asset> q = em.createQuery(query, Asset.class);

    return q;
}

It obviously needs to be parameterized to avoid SQL injection attack, but it's cool that it (nearly) doesn't have to know anything about the data being given it, that it'll just assemble a query from the fields it was given. It does need to know which fields it's doing what with--which fields are "like" versus "equals", how to handle "min" and "max" fields that define a range, etc. But that's not so bad.


Solution

  • Well, going the old fashioned way... The Servlets standard in Java defines object Request where there is Map (hash, dictionary) with the parameters, so you could access them something like

    public ModelAndView searchAssets(HttpRequest request) {
        StringBuilder builder = new StringBuilder();
        for (String param : request.getParameterNames()) {
            value = request.getParameter(param);
            builder.append(param);
            builder.append("="); // or LIKE
            builder.append("\'" + value "\'");
            builder.append(" AND ");
        }
        // deleting the last " AND "
        builder.delete(builder.length-5, builder.length);
        // In the entity create a method that executes a query with the string
        // If the parameter names are column names then you'd have to use a nativeQuery
        // You'd have to look it up in JPA
       List<Asset> list = Asset.search(builder.toString());
       // put the list in the ModelAndView
    }