Monday 25 November 2013

Forms, Request Parameters and CRUD

One of the most useful functions normally performed by a web framework is marshaling parameters between their String form on the request and other types which are more useful in your business layer. But how can you achieve this without a web framework?

In the framework world, this is often done by treating an Action class as a Java Bean and setting the parameters as properties on it either by matching the name of the parameter with the property or using annotations. 

Going the other way, in the case of editing an object using an HTML form, the form can be populated or re populated if a validation error occurs using the framework's custom tags or other HTML generation method to automatically format the values in your form.

In my No Framework application I want the same thing, but I don't want to use reflection or annotations in my Java classes or custom tags in my jsps. I would like to use standard HTML tags in my forms. 
In order to accomplish this, I've created a technique which is a kind of 'Just in time' parameter parsing. This harks back to the old days where we used to wrap the HTTPServletRequest object and have method calls such as getInteger("id") which would fetch id from the request params and parse it as an Integer. 

This is not a bad technique but I don't want to wrap the request as this would mean either wrapping it in every servlet method, or subclassing HttpServletRequest and using a filter to replace the request with my subclassed version which would mean always casting the request in my servlets. Neither of which I want. 

My solution is to create some helpers I can use to rapidly parse and reformat my request parameters. The helpers can also validate the parameters in a flexible way. Using the Id example, I would use my IntegerParam class:

IntegerParam id = new IntegerParam("id",request).require();

This single line performs all the functions I require. It parses the id parameter, and validates it as an int. The require() call additionally validates that it has been provided. Any validation errors are thrown as validation exceptions. 

Here's the code for my Integer request parameter  helper:

import javax.servlet.http.HttpServletRequest;

public class IntegerParam extends BaseParamHelper<Integer> implements ParamHelper<Integer> {

    public IntegerParam(String fieldName, HttpServletRequest request) {
        super(fieldName, request);
    }

    @Override
    public String format(Integer value) {
        return value.toString();
    }

    @Override
    public Integer parse(String val) throws ValidationException {
        Integer value = null;
        if (val != null && val.length() > 0) {
            try {
                value = Integer.valueOf(val);
            } catch (NumberFormatException nfe) {
                throw new ValidationException(getParameterName(),"This doesn't seem to be a valid number" );
            }
        } else {
            value = null;
        }
        return value;
    }

    public IntegerParam range(int min, int max, String msg) throws ValidationException {
        if (value() < min || value() > max) {
            throw new ValidationException(getParameterName(), msg);
        }
        return this;
    }
}

Notice that there is a custom validation added, which can be used to ensure the Integer is within a range. Here is the BaseParamHelper that can be extended to create ParamHelpers for any type.

import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;

public abstract class BaseParamHelper<T> implements ParamHelper<T> {
    private final String parameterName;
    private final HttpServletRequest request;
    private T value;

    public BaseParamHelper(String fieldName, HttpServletRequest request) {
        this.parameterName = fieldName;
        this.request = request;
    }

    public String getParameterName() {
        return parameterName;
    }

    public ParamHelper<T> require(String message) throws ValidationException {
        if (value() == null)
            throw new ValidationException(getParameterName(), message);
        return this;
    }

    public T value() throws ValidationException {
        if (value == null)
            value = parse(request.getParameter(parameterName));
        return value;
    }

    public List<T> values() throws ValidationException {
        List<T> values = new ArrayList<T>();
        String[] strValues = request.getParameterValues(parameterName);
        for (String str : strValues) {
            values.add(parse(str));
        }
        return values;
    }

    public void set(T value) {
        this.value = value;
        if (value != null)
            request.setAttribute(parameterName, format(value));
        else
            request.setAttribute(parameterName, null);
    }

    public void push() throws ValidationException {
        set(value());
    }

    public abstract T parse(String val) throws ValidationException;

    public abstract String format(T value);

}

This base class is where the require() validation is provided, subclasses must implement the parse() and format() methods so they have full control over marshalling between String and their wrapped type. With this and any custom validation, you don't have to use it.  In the validation methods, the original ParamHelper is returned, allowing you to chain validation calls before calling value(). There is also a values() method that can return a list if there is likely to be more than one value for a particular parameter.

If I'm using this id to pull a record from the database I can use it straightaway. I'm using jOOQ here.

Product product = getDSL().select()
.from(PRODUCT)
.where(PRODUCT.ID.eq(id.value()))
.fetchOne()
.into(Product.class);

Now, the second part is to put the product properties back onto the request (as String request attributes) so that I can output them in my jsp:

new StringParam("name",request).set(product.getName());
new DecimalParam("price",request).set(product.getPrice());
new StringParam("description",request).set(product.getDetails());



By calling set() here I'm setting the value of each parameter, and in doing so, the parameter helper will format the value into a string and put it on the request as a request attribute (accessible in the JSP).

Putting it all together:

@WebServlet("/product/form")
public class ProductFormServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    IntegerParam id = new IntegerParam("id",request).require("Please specify an id.");
    Product product = getDSL().select()
        .from(PRODUCT)
        .where(PRODUCT.ID.eq(id.value()))
        .fetchOne()
        .into(Product.class);

    new StringParam("name",request).set(product.getName());
    new DecimalParam("price",request).set(product.getPrice());
    new StringParam("description",request).set(product.getDetails());

           request.getServletContext().getRequestDispatcher("/product/edit.jsp").forward(request, response);
}


The form in the JSP will look like this:

<form action="/product/update">
<label for="name">Product Name</label>:<input type="text" name="name" value="${name}"/>
<label for="description">Description</label>:<input type="text" name="description" value="${description}"/>
<label for="price">Price</label>:<input type="text" name="price" value="${price}"/>
</form>


No custom form tags, and no formatting tags because those parameter helpers perform the formatting when setting them as request attributes.

You are probably thinking that it looks a bit repetitive and that you'd rather have a framework perform these some of this. But bear with me. In the code above I'm suggesting having a database call directly in my servlet. By doing so I'm admitting that this servlet serves no other purpose than fetching data from the database and populating a form. I'm mapping the fields on my form with the fields in the database right there in the servlet. This is saying: 'This is how I want the database fields to be mapped to the user interface in this request' but also 'I might want a different mapping in other requests'. This mapping has to be done somewhere. You might do this by creating Domain Objects or Business Objects that are Java POJOs that can be automatically populated from the database (JPA, Hibernate). But that creates a direct dependency between your database structure and your front end. Either you accept this, or you create additional layers in between such as FormBeans, DAOs, View objects etc. 

I don't want either those additional POJOS or any direct coupling between my database and my user interface, since I want absolute freedom in designing both of those parts of the system. 

Here's what the update servlet looks like:

@WebServlet("/product/update")
public class UpdateProductServlet extends HttpServlet {

    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
   IntegerParam id = new IntegerParam("id",request);
   
   DSLContext dsl = getDSL();
   
   Product product;    
   if(id.value()!=null){
       product = dsl.select()
               .from(PRODUCT)
               .where(PRODUCT.ID.eq(id.value()))
               .fetchOne()
               .into(Product.class);
   }else{
       product=new Product();
   }
   product.setName(new StringParam("name",request).require("You need to specify a name for the product").value());
   product.setPrice(new DecimalParam("price",request).require("Please provide a price for the product").value());
   product.setDetails(new StringParam("description",request).value(); //not a required field
   
        if (id.value() != null)
            dsl.executeUpdate(dsl.newRecord(PRODUCT,product));
        else
            dsl.executeInsert(dsl.newRecord(PRODUCT,product));
        
        request.getServletContext()
            .getRequestDispatcher("/success.jsp")
            .forward(request, response);
}
}

Once again we're using the parameter helpers to parse and validate the parameters. Validation exceptions would be caught in a Filter, which could either automatically forward back to the requesting URL, or return a JSON String with the validation error if the form was submitted over AJAX.

Here I've covered a number of techniques in this post that I've found help me on the journey remove the shackles of a web application framework. Here's a quick summary:

  • Helper classes to handle getting parameters from the request, parsing, validating and putting them back as String request attributes. This avoids relying on a framework to do this using reflection.
  • Helper classes to handle putting properties onto the request as String request attributes. This allows you to output them in your jsp without using special tags provided by the framework, or having to format values as you insert them into normal HTML.
  • Last minute validation means you are validating the data exactly where it needs to be validated, just before it gets put into the database.
  • For CRUD operations don't be scared to use jOOQ to make database calls directly in your servlets. This avoids creating repetitive POJO-populating logic.
I realise what I'm advocating here with this last point is rather controversial and goes against years of dogma of layer separation. However the layers are separate here, as separate as they need to be. I'm sacrificing physical separation to save typing, but I feel comfortable doing this because I know my user interface design is totally decoupled from the database design and the responsibility of my servlet classes are absolutely clear. Yes, I'm making the assumption that my data will always come from a database that is supported by jOOQ (and I won't be able to use dummy objects to test my logic). Yes I'm saying I won't ever want to reuse the code that's in these servlets. But I'm willing to make these sacrifices because it will keep my application small, neat, quick to develop and predictable. 

No comments:

Post a Comment