Liz Douglass

Posts Tagged ‘Freemarker

Java LinkBuilder

with one comment

Shortly before Christmas Tom and I decided to try to remove logic from some project Freemarker templates. Our first step was to move away from building up URIs inside templates, by creating some sort of Java URI builder.

Background:
We had lots of Freemarker template snippets that looked like this:

[#if member.someType.value != 'Foo']
     <div>[@macros.link_to 'Bar reports' 'member/report?id=${id}&amp;type=M&amp;database=${database}' /]</div>
[/#if]

All of them used the link_to Freemarker macro, which was defined as:

[#macro link_to caption path target='_top']
    [#if path?starts_with("/")]
        [#assign newPath=path?substring(1) /]
    [#else]
        [#assign newPath=path /]
    [/#if]
    <a href="[@spring.url '/appSecureServletPath/${newPath}' /]" target="${target}">${caption}</a>
[/#macro]

What we wanted to achieve first up:

Our aim was to replace all the uses of the link_to macro and instead generate all the URIs in Java and then add them to the model. We wanted to move a template usage like this:

<a href="[@spring.url contactDetailsChange?html /]">Change your contact details</a>

Where the link is added in the controller like so:

modelAndView.addObject("contactDetailsChange", linkBuilder.linkTo(ContactDetailsController.class));

Creating the LinkBuilder:

We did an assessment of all the URI endpoints in our project and realised that:
– We have some controller classes with a request mapping, some controllers that have methods with request mappings and some controllers with a combination of both class and method mappings.
– We have some controllers with more than one GET request method mapping and therefore could not assume only one request method mapping per controller.

We test drove a LinkBuilder interface and a DefaultLinkBuilder implementation for all the combinations we found in the controllers. The idea was to link from one handler class/method to another without being concerned about the specific URI mapped paths.

We also included methods that return a ModelAndView for redirecting and forward from a controller. Some of the methods also take a QueryString microtype (see below).

This is the LinkBuilder interface:

import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

public interface LinkBuilder {

    String linkTo(Class<?> controller, String methodName);

    String linkTo(Class<?> controller, RequestMethod method);

    String linkTo(Class<?> controller, String methodName, QueryString query);

    String linkTo(Class<?> controller, RequestMethod method, QueryString query);

    String linkToGet(Class<?> controller);

    String linkToGet(Class<?> controller, QueryString query);

    String forwardTo(Class<?> controller, String methodName);

    ModelAndView redirectTo(Class<?> controller, String method);

    ModelAndView redirectTo(Class<?> controller);

    ModelAndView redirectTo(Class<?> controller, String method, QueryString query);

    ModelAndView redirectTo(Class<?> controller, QueryString query);
}

And the DefaultLinkBuilder:

import org.apache.commons.lang.ArrayUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import java.lang.reflect.Method;

public class DefaultLinkBuilder implements LinkBuilder {

    private final String prefix;

    public DefaultLinkBuilder() {
        this("");
    }

    public DefaultLinkBuilder(String prefix) {
        this.prefix = prefix;
    }

    public String linkTo(Class<?> controller, String methodName) {
        return prefix + getControllerUrl(controller) + getMethodUrl(controller, methodName);
    }

    public String linkTo(Class<?> controller, RequestMethod method) {
        return prefix + getControllerUrl(controller) + getMethodUrl(controller, method);
    }

    public String linkTo(Class<?> controller, String methodName, QueryString query) {
        return linkTo(controller, methodName) + "?" + query;
    }

    public String linkTo(Class<?> controller, RequestMethod method, QueryString query) {
        return linkTo(controller, method) + "?" + query;
    }

    public String linkToGet(Class<?> controller) {
        return linkTo(controller, RequestMethod.GET);
    }

    public String linkToGet(Class<?> controller, QueryString query) {
        return linkTo(controller, RequestMethod.GET, query);
    }

    public String forwardTo(Class<?> controller, String methodName) {
        return "forward:" + linkTo(controller, methodName);
    }

    public ModelAndView redirectTo(Class<?> controller, String method) {
        return new ModelAndView("redirect:" + linkTo(controller, method));
    }

    public ModelAndView redirectTo(Class<?> controller) {
        return new ModelAndView("redirect:" + linkTo(controller, RequestMethod.GET));
    }

    public ModelAndView redirectTo(Class<?> controller, String method, QueryString query) {
        ModelAndView mv = redirectTo(controller, method);
        query.addToModel(mv.getModel());
        return mv;
    }

    public ModelAndView redirectTo(Class<?> controller, Context context) {
        QueryString queryString = context.asQueryString();
        return redirectTo(controller, queryString);
    }

    public ModelAndView redirectTo(Class<?> controller, QueryString query) {
        ModelAndView mv = redirectTo(controller);
        query.addToModel(mv.getModel());
        return mv;
    }

    private String getControllerUrl(Class<?> controller) {
        RequestMapping annotation = AnnotationUtils.findAnnotation(controller, RequestMapping.class);
        if (annotation != null) {
            return getFirstValue(annotation);
        }
        return "";
    }

    private String getMethodUrl(Class<?> controller, String methodName) {
        Method[] methods = controller.getMethods();
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
                if (annotation != null) {
                    return getFirstValue(annotation);
                }
            }
        }
        throw new IllegalArgumentException("Cannot find method with name " + methodName
                + " with a RequestMapping annotation on controller " + controller.getName());
    }

    private String getMethodUrl(Class<?> controller, RequestMethod requestMethod) {
        Method[] methods = controller.getMethods();
        for (Method method : methods) {
            RequestMapping annotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
            if (annotation != null) {
                if (ArrayUtils.contains(annotation.method(), requestMethod)) {
                    return getFirstValue(annotation);
                }
            }
        }
        throw new IllegalArgumentException("Cannot find method that can handle " + requestMethod
                + " requests on controller " + controller.getName());
    }

    private String getFirstValue(RequestMapping annotation) {
        String[] value = annotation.value();
        if (value.length > 0) {
            return value[0];
        }
        return "";
    }
}

We have two subclasses of the DefaultLinkBuilder the SecureLinkBuilder and the UnsecureLinkBuilder. These classes simply set the appropriate servlet path as the prefix.

The QueryString class knows how to add itself into the model:

import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.util.EncodingUtil;

import java.util.List;
import java.util.Map;

public class QueryString {

    private final List<NameValuePair> pairs = Lists.create();

    public QueryString add(String name, String value) {
        pairs.add(new NameValuePair(name, value));
        return this;
    }

    public boolean isEmpty() {
        return pairs.isEmpty();
    }

    public NameValuePair[] toArray() {
        return pairs.toArray(new NameValuePair[pairs.size()]);
    }

    public void addToModel(Map<String, Object> model) {
        for (NameValuePair pair : pairs) {
            model.put(pair.getName(), pair.getValue());
        }
    }

    @Override
    public String toString() {
        return EncodingUtil.formUrlEncode(toArray(), "UTF-8");
    }
}

The creation of the LinkBuilder and the implementations allowed us to go through all the controllers and add links into the models. We removed a lot of string concatenation in templates doing this.

Written by lizdouglass

January 5, 2010 at 10:13 am

Posted in Uncategorized

Tagged with , ,