Liz Douglass

Part 3: Recursive menu building

leave a comment »

Part 3 of the Freemarker cleanup involved refactoring menu creation. The application has several menus that appear on the left side of the browser window. These menus are either nested inside eachother or stacked on top of one another.

Before part 3, the creation of the menus was done by one template called main.ftl. This template included other templates (and so on). This was main.ftl:

 
[#ftl]

[#if user.member]
	[#include "/member-menu.ftl"]
[#else]
	[#include "/cross-role-menu.ftl"]
[/#if]
[#if someCondition?? ]
	[#include "/some-menu.ftl"]
[/#if ]
	[#if foo.id !=  user.id]
		[#include "/foo-menu.ftl"]
[/#if ]

All of the menu templates had some conditional logical statements in them – in fact, there was probably an average of about a dozen per template. The only place that these logic statements were tested was in the Selenium tests – obviously not ideal.

The aim of this refactoring was to get rid of all the conditional logic in the templates. The logic would be replaced with tested Java code. This made it possible to render the menus from one single recursive menu template:

 
[#ftl]
[#import "/spring.ftl" as spring /]

[#if menu??]
    [@buildMenu menu=menu depth=0/]
[/#if]

[#macro buildMenu menu depth]
    [#list menu.menuEntries as menuEntry]
        [#if menuEntry.type == "LINK"]
            <div>
                <a href="[@spring.url menuEntry.link?html /]">${menuEntry.caption?html}</a>
            </div>
        [#else]
            [#if menuEntry.menuEntries?size > 0]
                [#if depth > 0]
                    <h3>${menuEntry.caption}</h3>
                    <div id="${menuEntry.name}" class="menu_sub_block">
                [#else]
                    <div id="${menuEntry.name}" class="menu_block">
                    <h5>${menuEntry.caption}</h5>
                [/#if]
                [@buildMenu menu=menuEntry depth=depth+1/]
                </div>
            [/#if]
        [/#if]
    [/#list]
[/#macro]

The new template builds all the menus from the menu model entry. This is added into the model in the MenuInterceptor. This interceptor has dependencies on four factories that create the required menus. The postHandle method creates a rootMenu that contains the other menus:

 
public class MenuInterceptor extends HandlerInterceptorAdapter {
    private FooMenuFactory fooMenuFactory;
    private CrossRoleMenuFactory crossRoleMenuFactory;
    private MemberMenuFactory memberMenuFactory;
    private SomeMenuFactory someMenuFactory;

    @Autowired
    public void setFooMenuFactory(FooMenuFactory fooMenuFactory) {
        this.fooMenuFactory = fooMenuFactory;
    }

    // omitted setters for the other factories

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mv)
            throws Exception {

        if (mv != null) {
            User user = (User) request.getAttribute(RequestAttributeNames.user.name());

            Menu rootMenu = new Menu("menu");
            Menu memberMenu = memberMenuFactory.createMemberMenu(user);
            Menu crossRoleMenu = crossRoleMenuFactory.createCrossRoleMenu(user);
            Menu someMenu = someMenuFactory.createSomeMenu(user);
            Menu fooMenu = fooMenuFactory.createFooMenu(user);
            rootMenu.addEntries(memberMenu, crossRoleMenu, someMenu, fooMenu);

            mv.addObject("menu", rootMenu);
        }
    }
}

Each of the menu factories creates a Menu object. The Menu class has a list of MenuEntry objects. There are two implementations of the MenuEntry interface: Link and Menu:

The MenuEntry interface:

 
public interface MenuEntry {

    String getCaption();

    MenuEntryType getType();
}

Menu:

 
public class Menu implements MenuEntry {
    private String caption = "";

    private List<MenuEntry> menuEntries;
    private final String name;

    public Menu(String name) {
        this.name = name;
        menuEntries = Lists.create();
    }

    public void setCaption(String caption) {
        this.caption = caption;
    }

    public void addEntry(MenuEntry menuItem) {
        menuEntries.add(menuItem);
    }

    public String getCaption() {
        return caption;
    }

    public List<MenuEntry> getMenuEntries() {
        return menuEntries;
    }

    public void addEntries(MenuEntry... links) {
        menuEntries.addAll(Arrays.asList(links));
    }

    public MenuEntryType getType() {
        return MenuEntryType.MENU;
    }

    public String getName() {
        return name;
    }
}

… and Link:

 
public class Link implements MenuEntry {

    private final String captionText;
    private final SecureLinks.Link link;

    public SecureContextLink(String captionText, SecureContextLinks.Link link) {
        this.captionText = captionText;
        this.link = link;
    }

    public String getCaption() {
        return captionText;
    }

    public String getLink() {
        return link.toString();
    }

    public MenuEntryType getType() {
        return MenuEntryType.LINK;
    }
}

Each of the factories creates a menu using the logic that was previously in the Freemarker templates. Each factory was developed using TDD and looks a bit like this:

 
public class FooMenuFactory {
    private FooRepository fooRepository;
    private final SecureContextLinks links = new SecureContextLinks();

    public FooMenuFactory(FooRepository fooRepository) {
        this.fooRepository = fooRepository;
    }

    public Menu createFooMenu(User user) {
        Menu fooMenu = new Menu("foo_menu");

        fooMenu.setCaption(user.getDisplayName());

        if (context.isMember()) {
           fooMenu.addEntry(new SecureContextLink("Summary", links.getSomeSummary()));
        }

        // add other links

        return fooMenu;
    }
}

Moving to this style of menu creation reduced 7 templates down to one recursive template. All of the logic that was previously in the templates was moved into Java and was unit tested. (Yay!)

Advertisements

Written by lizdouglass

January 11, 2010 at 6:19 am

Posted in Uncategorized

Tagged with ,

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: