指令

Java程序员可以使用 TemplateDirectiveModel 接口在Java代码中实现自定义指令。详情可以参加API文档。

Note:

TemplateDirectiveModel 在 FreeMarker 2.3.11 版本时才加入, 来代替快被废弃的 TemplateTransformModel

示例 1

我们要实现一个指令, 这个指令可以将在它开始标签和结束标签之内的字符都转换为大写形式。 就像这个模板:

foo
<@upper>
  bar
  <#-- All kind of FTL is allowed here -->
  <#list ["red", "green", "blue"] as color>
    ${color}
  </#list>
  baaz
</@upper>
wombat

将会输出:

foo
  BAR
    RED
    GREEN
    BLUE
  BAAZ
wombat

下面是指令类的源代码:

package com.example;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;

import freemarker.core.Environment;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;

/**
 *  FreeMarker user-defined directive that progressively transforms
 *  the output of its nested content to upper-case.
 *  
 *  
 *  <p><b>Directive info</b></p>
 * 
 *  <p>Directive parameters: None
 *  <p>Loop variables: None
 *  <p>Directive nested content: Yes
 */
public class UpperDirective implements TemplateDirectiveModel {
    
    public void execute(Environment env,
            Map params, TemplateModel[] loopVars,
            TemplateDirectiveBody body)
            throws TemplateException, IOException {
        // Check if no parameters were given:
        if (!params.isEmpty()) {
            throw new TemplateModelException(
                    "This directive doesn't allow parameters.");
        }
        if (loopVars.length != 0) {
                throw new TemplateModelException(
                    "This directive doesn't allow loop variables.");
        }
        
        // If there is non-empty nested content:
        if (body != null) {
            // Executes the nested body. Same as <#nested> in FTL, except
            // that we use our own writer instead of the current output writer.
            body.render(new UpperCaseFilterWriter(env.getOut()));
        } else {
            throw new RuntimeException("missing body");
        }
    }
    
    /**
     * A {@link Writer} that transforms the character stream to upper case
     * and forwards it to another {@link Writer}.
     */ 
    private static class UpperCaseFilterWriter extends Writer {
       
        private final Writer out;
           
        UpperCaseFilterWriter (Writer out) {
            this.out = out;
        }

        public void write(char[] cbuf, int off, int len)
                throws IOException {
            char[] transformedCbuf = new char[len];
            for (int i = 0; i < len; i++) {
                transformedCbuf[i] = Character.toUpperCase(cbuf[i + off]);
            }
            out.write(transformedCbuf);
        }

        public void flush() throws IOException {
            out.flush();
        }

        public void close() throws IOException {
            out.close();
        }
    }

}

现在我们需要创建这个类的实例, 然后让这个指令在模板中可以通过名称"upper"来访问 (或者是其它我们想用的名字)。一个可行的方案是把这个指令放到数据模型中:

root.put("upper", new com.example.UpperDirective());

但更好的做法是将常用的指令作为 共享变量 放到 Configuration 中。

当然也可以使用 内建函数new 将指令放到一个FTL库(宏的集合,就像在模板中, 使用 includeimport )中:

<#-- Maybe you have directives that you have implemented in FTL -->
<#macro something>
  ...
</#macro>

<#-- Now you can't use <#macro upper>, but instead you can: -->
<#assign upper = "com.example.UpperDirective"?new()>

示例 2

我们来创建一个指令,这个指令可以一次又一次地执行其中的嵌套内容, 这个次数由指定的数字来确定(就像 list 指令), 可以使用<hr>将输出的重复内容分开。 这个指令我们命名为"repeat"。示例模板如下:

<#assign x = 1>

<@repeat count=4>
  Test ${x}
  <#assign x++>
</@repeat>

<@repeat count=3 hr=true>
  Test
</@repeat>

<@repeat count=3; cnt>
  ${cnt}. Test
</@repeat>

输出为:

  Test 1
  Test 2
  Test 3
  Test 4

  Test
<hr>  Test
<hr>  Test

  1. Test
  2. Test
  3. Test
 

指令的实现类为:

package com.example;
import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.Map;

import freemarker.core.Environment;
import freemarker.template.SimpleNumber;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;

/**
 * FreeMarker user-defined directive for repeating a section of a template,
 * optionally with separating the output of the repetations with
 * <tt>&lt;hr></tt>-s.
 *
 * 
 * <p><b>Directive info</b></p>
 * 
 * <p>Parameters:
 * <ul>
 *   <li><code>count</code>: The number of repetations. Required!
 *       Must be a non-negative number. If it is not a whole number then it will
 *       be rounded <em>down</em>.
 *   <li><code>hr</code>: Tells if a HTML "hr" element could be printed between
 *       repetations. Boolean. Optional, defaults to <code>false</code>. 
 * </ul>
 *
 * <p>Loop variables: One, optional. It gives the number of the current
 *    repetation, starting from 1.
 * 
 * <p>Nested content: Yes
 */
public class RepeatDirective implements TemplateDirectiveModel {
    
    private static final String PARAM_NAME_COUNT = "count";
    private static final String PARAM_NAME_HR = "hr";
    
    public void execute(Environment env,
            Map params, TemplateModel[] loopVars,
            TemplateDirectiveBody body)
            throws TemplateException, IOException {
        
        // ---------------------------------------------------------------------
        // Processing the parameters:
        
        int countParam = 0;
        boolean countParamSet = false;
        boolean hrParam = false;
        
        Iterator paramIter = params.entrySet().iterator();
        while (paramIter.hasNext()) {
            Map.Entry ent = (Map.Entry) paramIter.next();
            
            String paramName = (String) ent.getKey();
            TemplateModel paramValue = (TemplateModel) ent.getValue();
            
            if (paramName.equals(PARAM_NAME_COUNT)) {
                if (!(paramValue instanceof TemplateNumberModel)) {
                    throw new TemplateModelException(
                            "The \"" + PARAM_NAME_HR + "\" parameter "
                            + "must be a number.");
                }
                countParam = ((TemplateNumberModel) paramValue)
                        .getAsNumber().intValue();
                countParamSet = true;
                if (countParam < 0) {
                    throw new TemplateModelException(
                            "The \"" + PARAM_NAME_HR + "\" parameter "
                            + "can't be negative.");
                }
            } else if (paramName.equals(PARAM_NAME_HR)) {
                if (!(paramValue instanceof TemplateBooleanModel)) {
                    throw new TemplateModelException(
                            "The \"" + PARAM_NAME_HR + "\" parameter "
                            + "must be a boolean.");
                }
                hrParam = ((TemplateBooleanModel) paramValue)
                        .getAsBoolean();
            } else {
                throw new TemplateModelException(
                        "Unsupported parameter: " + paramName);
            }
        }
        if (!countParamSet) {
                throw new TemplateModelException(
                        "The required \"" + PARAM_NAME_COUNT + "\" paramter"
                        + "is missing.");
        }
        
        if (loopVars.length > 1) {
                throw new TemplateModelException(
                        "At most one loop variable is allowed.");
        }
        
        // Yeah, it was long and boring...
        
        // ---------------------------------------------------------------------
        // Do the actual directive execution:
        
        Writer out = env.getOut();
        if (body != null) {
            for (int i = 0; i < countParam; i++) {
                // Prints a <hr> between all repetations if the "hr" parameter
                // was true:
                if (hrParam && i != 0) {
                    out.write("<hr>");
                }
                
                // Set the loop variable, if there is one:
                if (loopVars.length > 0) {
                    loopVars[0] = new SimpleNumber(i + 1);
                }
                
                // Executes the nested body (same as <#nested> in FTL). In this
                // case we don't provide a special writer as the parameter:
                body.render(env.getOut());
            }
        }
    }

}

注意

TemplateDirectiveModel 对象通常不应该是有状态的,这一点非常重要。 一个经常犯的错误是存储指令的状态然后在对象的属性中调用执行。 想一下相同指令的嵌入调用,或者指令对象被用作共享变量, 并通过多线程同时访问。

不幸的是, TemplateDirectiveModel 不支持传递参数的位置(而不是参数名称)。从 FreeMarker 2.4 版本开始,它将被修正。