Freemarker Mixins

Version 2

    Introduction

     

    This post shows a pattern for solving the problem:

    How do you modify or add functionality to a page in an unobtrusive and repeatable manner?

    By repeatable, I mean that multiple plugins or extensions can apply the same pattern to the same page without overriding or canceling one another.

     

    The Pattern

     

    This pattern takes advantage of the fact that Sitemesh allows you to merge multiple <head/> sections in to a single page.  We will create a small FTL that contains our custom code in a <head/> section, include the original page and then Sitemesh will combine them for us.

     

    Note: The example below uses the Struts ConfigurationProvider pattern that was initially outlined in How To: Add Struts Interceptors at Runtime.

     

    Modify the target Struts action to point to your new FTL

     

    public class ViewProfileConfigurationProvider extends BaseConfigurationProvider {
    
        @SuppressWarnings("unchecked")
        @Override
        public void loadPackages() throws ConfigurationException {
            ResultConfig result = getActionConfig("profile").getResults().get("success");
            result.getParams().put("nitroViewProfileLocation", result.getParams().get("location"));
            result.getParams().put("location", "/plugins/gamification/resources/templates/view-profile.ftl");
        }
    
    }
    
    
    
    

     

    I'm modifying the "success" result to point to my FTL while also saving the old location so I can reference it later.

    Add a Struts Interceptor to allow my FTL to see the old location

     

    public class ViewProfileInterceptor implements Interceptor {
    
        @Override
        public void destroy() {
        
        }
    
        @Override
        public void init() {
        
        }
    
        @SuppressWarnings("unchecked")
        @Override
        public String intercept(ActionInvocation invocation) throws Exception {
            ResultConfig result = invocation.getProxy().getConfig().getResults().get("success");
        
            invocation.getStack().getContext().put("nitroViewProfileLocation", result.getParams().get("nitroViewProfileLocation"));
        
            return invocation.invoke();
        }
    
    }
    
    
    
    

     

    The interceptor is setting the original result location in to the freemarker context under the nitroViewProfileLocation variable.  You can also use this Interceptor to inject other things in to the Freemarker context for the page to use.

     

    Write your new FTL

     

    (example, /plugins/gamification/resources/templates/view-profile.ftl):

     

    <#include "${nitroViewProfileLocation}"/>
    
    <#assign nitroClient = jiveContext.getSpringBean("nitroClient") />
    <#assign nitroClientConfiguration = jiveContext.getSpringBean("nitroClientConfiguration") />
    <#assign signature = nitroClient.getSignature(targetUser.ID) />
    
    <html>
        <head>
            <link href="<@resource.url value='/plugins/gamification/resources/styles/lib/n4jive/n4jive.bio.css'/>" rel="stylesheet" type="text/css" />
        
            <@resource.javascript file="/plugins/gamification/resources/script/apps/shared/models/userService.js" />
            <@resource.javascript file="/plugins/gamification/resources/script/apps/profile/main.js" />
            <@resource.javascript file="/plugins/gamification/resources/script/apps/profile/models/profile_source.js" />
            <@resource.javascript file="/plugins/gamification/resources/script/apps/profile/views/profile_view.js" />
        
            <@resource.javascript>
                var nitroProfile = new jive.nitro.profile.Main({
                    apiKey: "${nitroClientConfiguration.apiKey}",
                    timeStamp: "${signature.timestamp?c}",
                    signature: "${signature.signatureValue}",
                    server: "${nitroClientConfiguration.baseUrl}",
                    userID: "${user.ID?c}",
                    targetUserID: "${targetUser.ID?c}",
                    useShort: false
                });
            </@resource.javascript>
        </head>
    
    </html>
    
    
    
    

     

    You can see that the very first thing it does is <#include/> the original location.  Sitemesh will then merge this header definition in to the main page as if the original FTL included it.

     

    Advantages/Disadvantages over Plugin JavaScript

     

    In my opinion, this pattern has a few advantages over using the straight plugin JavaScript method:

     

    • It targets a single page instead of running on every page.
    • Your code executes on the server side before being returned to the client.

     

    The obvious disadvantage to this pattern is the slight complexity in implementing it.  However, with some slight changes in core with regards to how the Struts configuration can be modified at runtime, I think this can be obviated.

     

    Thanks to Kevin.Conaway for the great write-up!