-1

I'm doing a simple MessageRenderer.

It's specification:

  • Render message based on an Context (it's a map that's contains all key/value pair parameters)
  • Supports simple render such as: Your username is << username >>. Assume username in the context is barcelona and the result will be Your username is Barcelona.
  • Supported function-like object. Example: Current time is << now() >>, now(): is an object that will returns a string of current date time. And result will be: Current time is 2011-05-30
  • Each parameter of function can also be templated: Current time is << now( << date_format >> ) >> . This template returns a string of current date time with format is the value of key 'date_format' retrieved from the Context. Assume date_format in Context is dd/MM/yyyy and the result will be: Current time is 30/05/2011
  • Each parameter of function can also be templated with a different method call: Time is << now_locale ( << getLocale() >> ). Assume that getLocale() is an function object that will be return a locale is en_US and the result will be: Time is 2011/05/30 11:20:34 PM
  • Template can be nested. Example: Your user name is << << username >> >>. It means, Key username has value param1, Key param1 has value is barcelona so the final result will be: Your user name is Barcelona.

My classes and interfaces:

RenderContext.java

public interface RenderContext {
    public String getParameter(String key);
}

MessageRenderer.java

public interface MessageRenderer {
      public String render(String s, RenderContext... context);    
}

MethodExpressionEvaluator.java

// Using this class to implements the method evaluation, such as now(), now_locale()
public interface MethodExpressionEvaluator {
      public String evaluate(String[] methodParams, RenderContext... context);
}

AbstractMessageRenderer.java

public abstract class AbstractMessageRenderer implements MessageRenderer {

public static final String DEFAULT_NULL = "###";
public static final String PLACEHOLDER_START_TOKEN = "<<";
public static final String PLACEHOLDER_END_TOKEN = ">>";

protected int lenPlaceholderStartToken = 0;
protected int lenPlaceholderEndToken = 0;
protected String nullToken;
protected String placeholderStartToken;
protected String placeholderEndToken;
protected boolean escape = true;

public AbstractMessageRenderer() {
    placeholderStartToken = PLACEHOLDER_START_TOKEN;
    placeholderEndToken = PLACEHOLDER_END_TOKEN;
    lenPlaceholderStartToken = placeholderStartToken.length();
    lenPlaceholderEndToken = placeholderEndToken.length();
    nullToken = DEFAULT_NULL;
}

public String getNullToken() {
    return nullToken;
}

public void setNullToken(String defaultNull) {
    this.nullToken = defaultNull;
}

public String getPlaceholderStartToken() {
    return placeholderStartToken;
}

public void setPlaceholderStartToken(String placeholderStartToken) {
    this.placeholderStartToken = placeholderStartToken;
    lenPlaceholderStartToken = placeholderStartToken.length();
}

public String getPlaceholderEndToken() {
    return placeholderEndToken;
}

public void setPlaceholderEndToken(String placeholderEndToken) {
    this.placeholderEndToken = placeholderEndToken;
    lenPlaceholderEndToken = placeholderEndToken.length();
}

public boolean isEscape() {
    return escape;
}

public boolean getEscape() {
    return escape;
}

public void setEscape(boolean escape) {
    this.escape = escape;
}

public String getParam(String key, RenderContext... context) {

    if(context != null)
    {
        for(RenderContext param:context)
        {
            if(param != null)
            {
                String value = param.getParameter(key);

                if(!StringUtil.isEmpty(value))
                {
                    return value;
                }
            }
        }
    }

    return nullToken;
}

public String render(String s, RenderContext... context) {

    // handle trivial cases of empty template or no placeholders
    if (s == null)
    {
        Log4j.app.debug("Message is null in template. Cannot render null message.");
        return nullToken;
    }

    if (context == null)
    {
        Log4j.app.debug("RenderContext is null. Cannot render message with null RenderContext.");
        return nullToken;
    }

    if (s.indexOf(placeholderStartToken) < 0)
    {
        return s;
    }

    String msg = nullToken;

    try
    {
        // private int renderTemplate(Renderable r, String src, StringBuffer dst, String nil, int i, String[] marks, StringBuffer end,boolean escapes)
        msg = doRender(s, context);
    }
    catch (Exception e)
    {
        Log4j.app.error("Exception in rendering template: " + e.getMessage(), e);
        return nullToken;
    }

       return msg;
    }

protected abstract String doRender(String s, RenderContext... context);

}

MethodExpressionRenderer.java

public class MethodExpressionRenderer extends AbstractMessageRenderer {

    private boolean inSingleQuote = false;
    private boolean inDoubleQuote=false;
    private int placeholders;
    private Stack<String> methodStack;    
    private String[] endTokens;
    private String marker;
    private List<String> methodParams;
    private String prefix = "&";

    public MethodExpressionRenderer() {
        super();
        methodStack = new Stack<String>();
        marker = ",";
        endTokens = new String[] { placeholderEndToken, marker, "(", ")" };
        methodParams = new ArrayList<String>();
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getMarker() {
        return marker;
    }

    public void setMarker(String marker) {
        this.marker = marker;
        endTokens = new String[] { placeholderEndToken, marker };
    }

    @Override
    public void setPlaceholderEndToken(String placeholderEndToken) {
        super.setPlaceholderEndToken(placeholderEndToken);
        endTokens = new String[] { placeholderEndToken, marker };
    }

    protected String doRender(String s, RenderContext... context) {

        StringBuffer sb = new StringBuffer();

        try
        {            
            renderTemplate(s, sb, nullToken, 0, endTokens, null, context);
        } 
        catch (Exception e)
        {
            Log4j.app.error("Exception in rendering method expression message emplate: " + e.getMessage(), e);
            return nullToken;
        }

        return sb.toString();
    }

    private int renderTemplate(String src, StringBuffer dst, String nil, int i, String[] marks, StringBuffer end, RenderContext... context) {

        int len = src.length();

        while (i < len)
        {
            char c = src.charAt(i);

            if (escape)
            {
                if (c=='\\')
                {
                    i++;
                    char ch = src.charAt(i);

                    if(inSingleQuote)
                    {
                        if(ch=='\'')
                        {
                            inSingleQuote=false;
                        }
                    }
                    else if(inDoubleQuote)
                    {
                        if(ch=='"')
                        {
                            inDoubleQuote=false;
                        }
                    }
                    else
                    {
                        if(ch=='\'')
                        {
                            inSingleQuote=true;
                        }
                        else if(ch=='"')
                        {
                            inDoubleQuote=true;
                        }
                    }

                    dst.append(ch);
                    i++;
                    continue;
                }
            }

            if(inSingleQuote)
            {
                if(c=='\'')
                {
                    inSingleQuote=false;
                }
            }
            else if(inDoubleQuote)
            {
                if(c=='"')
                {
                    inDoubleQuote=false;
                }
            }
            else
            {
                if(c=='\'')
                {
                    inSingleQuote=true;
                }
                else if(c=='"')
                {
                    inDoubleQuote=true;
                }
            }

            // check for end marker
            if (marks != null && !inSingleQuote && !inDoubleQuote)
            {
                for (int m = 0; m < marks.length; m++)
                {
                    // If one of markers found
                    if (src.regionMatches(i, marks[m], 0, marks[m].length()))
                    {
                        // return marker if required
                        if (end != null)
                        {
                            end.append(marks[m]);
                        }

                        return i+marks[m].length();
                    }
                }
            }

            // check for start of placeholder
            if (src.regionMatches(i, placeholderStartToken, i, lenPlaceholderStartToken))
            {
                synchronized(this)
                {
                    ++placeholders;
                }

                i = renderPlaceholder(src, dst, nil, i, new ArrayList<String>(), context);
                continue;
            }

            // just add plain character

            if(c != '\'' && c!= '"')
            {
                dst.append(c);
            }

            i++;
        }

        return i;
    }

    private int renderPlaceholder(String src, StringBuffer dst, String nil, int i, List<String> params, RenderContext... context){

        StringBuffer token = new StringBuffer(); // placeholder token
        StringBuffer end = new StringBuffer();  // placeholder end marker
        String value;

        i = renderTemplate(src, token, nil, i+lenPlaceholderStartToken, endTokens, end);

        String sToken = token.toString().trim();        
        String sEnd = end.toString().trim();
        boolean isFunction = sEnd.equals("(");

        // This is method name
        if(isFunction && placeholders > methodStack.size())
        {   // Method
            synchronized(this)
            {
                methodStack.push(sToken); // put method into stack
            }
        }
        else if(!isFunction && (methodStack.size()==0) && sEnd.equals(placeholderEndToken)) // Single template param such as <<param>>
        {
            value = getParam(sToken, context);

            if(value != null)
            {
                if(value.trim().startsWith(placeholderStartToken))
                {
                    value = render(src, context);                    
                }

                dst.append(value);
                return i;
            }
        }

       // TODO: Process method parameters to invoke
       //.... ?????????

        // Found end method token ')'
        // Pop method out of stack to invoke
        if ( (methodStack.size() >0) && (sEnd.length() == 0 || sEnd.equals(")")))
        {
            String method = null;

            synchronized(this)
            {
                // Pop method out of stack to invoke
                method = methodStack.pop();
                --placeholders;
                dst.append(invokeMethodEvaluator(method, methodParams.toArray(new String[0]), context));
                methodParams.clear();
            }
        }

        return i;
    }

    // Currently this method just implement to test so it just printout the method name 
    // and its parameter
    // We can register MethodExpressionEvaluator to process
    protected String invokeMethodEvaluator(String method, String[] params, RenderContext... context){
        StringBuffer result = new StringBuffer();

        result.append("[ ")
              .append(method)
              .append(" ( ");

        if(params != null)
        {
            for(int i=0; i<params.length; i++)
            {
                result.append(params[i]);

                if(i != params.length-1)
                {
                    result.append(" , ");
                }
            }
        }

        result.append(" ) ")
              .append(" ] ");

        return result.toString();
    }

}

We can easily register more method to the renderer to invoke. Each method will be an object and can be reused. But I'm in trouble how to resolve the nested method parameter. Can anyone give me an advice how we can process nested template of method parameter to invoke??? The line has TODO. Will my code in on the right way???

Barcelona
  • 3
  • 1
Barcelona
  • 11
  • 1
  • I have trouble seeing what nested templates are supposed to do? Should `<< << username() >> >>` be turned into `<< sarnold() >>` and then the method `sarnold()` should be called? – sarnold May 30 '11 at 03:09
  • The template << << username >> >> means: It resolves the key username and then continue resolve value retrieved from that username key. Resolves deeply. – Barcelona May 30 '11 at 03:25
  • Whenever renderer meets the '<<', the template should be resolved....And renderer will resolve based on the RenderContext we supplied. – Barcelona May 30 '11 at 03:28
  • With function, as in above example, the count() function has one parameter and it be templated by call getTransId() function. Ex: << count ( << getTransId() >> ) >> . – Barcelona May 30 '11 at 03:37

2 Answers2

0

When you evaluate something like << count( << getTransId() >> ) >> you can either:

  • perform direct-evaluation as you parse, and push each function onto a stack, so that once you've evaluated getTransId() you pop the stack and use the return value (from the stack) as an argument for count(), or
  • you can build a parse tree to represent all the function calls that will be made, and then evaluate your parse tree after building it. (Building a tree probably doesn't buy you anything; since you're writing a template engine there is probably no high-level tree operation 'optimizations' that you could perform.)

An excellent little book I really enjoyed was Language Implementation Patterns by Parr. He walks through building simple to complex languages, and covers decisions like this in some depth. (Yes, he uses the ANTLR parser generator throughout, but your code looks like you're familiar enough with hand-generated parsers that different tools won't be a distraction for you.)

sarnold
  • 102,305
  • 22
  • 181
  • 238
0

I found the bug and fixed it.

This is my new source:

// AbstractMethodExpressionRenderer.java

public class AbstractMethodExpressionRenderer extends AbstractMessageRenderer {

    private boolean inSingleQuote = false;
    private boolean inDoubleQuote=false;
    private Stack<MethodExpressionDescriptor> functionStack;
    private String[] endTokens;
    private String marker;
    private String prefix = "~";

    public AbstractMethodExpressionRenderer() {
        super();        
        functionStack = new Stack<MethodExpressionDescriptor>();
        marker = ",";
        endTokens = new String[] { placeholderEndToken, "(", ")", };
    }

    private class MethodExpressionDescriptor {
        public List<String> params;
        public String function;

        public MethodExpressionDescriptor() {
            params = new ArrayList<String>();
        }

        public MethodExpressionDescriptor(String name) {
            this();
            this.function = name;
        }
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getMarker() {
        return marker;
    }

    public void setMarker(String marker) {
        this.marker = marker;
        endTokens = new String[] { placeholderEndToken, marker };
    }

    @Override
    public void setPlaceholderEndToken(String placeholderEndToken) {
        super.setPlaceholderEndToken(placeholderEndToken);
        endTokens = new String[] { placeholderEndToken, marker };
    }

    protected String doRender(String s, RenderContext... context) {

        StringBuffer sb = new StringBuffer();

        try
        {            
            renderTemplate(s, sb, nullToken, 0, endTokens, null, context);
        } 
        catch (Exception e)
        {
            Log4j.app.error("Exception in rendering method expression message emplate: " + e.getMessage(), e);
            return nullToken;
        }

        return sb.toString();
    }

    private int renderTemplate(String src, StringBuffer dst, String nil, int i, String[] marks, StringBuffer end, RenderContext... context) {

        int len = src.length();

        while (i < len)
        {
            char c = src.charAt(i);

            if (escape)
            {
                if (c=='\\')
                {
                    i++;
                    char ch = src.charAt(i);

                    if(inSingleQuote)
                    {
                        if(ch=='\'')
                        {
                            inSingleQuote=false;
                        }
                    }
                    else if(inDoubleQuote)
                    {
                        if(ch=='"')
                        {
                            inDoubleQuote=false;
                        }
                    }
                    else
                    {
                        if(ch=='\'')
                        {
                            inSingleQuote=true;
                        }
                        else if(ch=='"')
                        {
                            inDoubleQuote=true;
                        }
                    }

                    dst.append(ch);
                    i++;
                    continue;
                }
            }

            if(inSingleQuote)
            {
                if(c=='\'')
                {
                    inSingleQuote=false;
                }
            }
            else if(inDoubleQuote)
            {
                if(c=='"')
                {
                    inDoubleQuote=false;
                }
            }
            else
            {
                if(c=='\'')
                {
                    inSingleQuote=true;
                }
                else if(c=='"')
                {
                    inDoubleQuote=true;
                }
            }

            // check for end marker
            if (marks != null && !inSingleQuote && !inDoubleQuote)
            {
                for (int m = 0; m < marks.length; m++)
                {
                    // If one of markers found
                    if (src.regionMatches(i, marks[m], 0, marks[m].length()))
                    {
                        // return marker if required
                        if (end != null)
                        {
                            end.append(marks[m]);
                        }

                        return i+marks[m].length();
                    }
                }
            }

            // check for start of placeholder
            if (src.regionMatches(i, placeholderStartToken, 0, lenPlaceholderStartToken))
            {
                i = renderPlaceholder(src, dst, nil, i, new ArrayList<String>(), context);
                continue;
            }

            // just add plain character
            if(c != '\'' && c!= '"')
            {
                dst.append(c);
            }

            i++;
        }

        return i;
    }

    /**
     * Render a placeholder as follows:
     *
     * <<key>>: Simple render, key value map
     * <<function(<<param1>>, <<param2>>)>> : Function object render
     * 
     * @param src
     * @param dst
     * @param nil
     * @param i
     * @param params
     * @param context
     * @return
     */
    private int renderPlaceholder(String src, StringBuffer dst, String nil, int i, List<String> params, RenderContext... context){

        StringBuffer token = new StringBuffer(); // placeholder token
        StringBuffer end = new StringBuffer();  // placeholder end marker
        String value = null;

        // Simple key
        i = renderTemplate(src, token, nil, i+lenPlaceholderStartToken, endTokens, end, context);

        String sToken = token.toString().trim();        
        String sEnd = end.toString().trim();

        // This is method name
        if(sEnd.equals("("))
        {   // Method
            functionStack.add(new MethodExpressionDescriptor(sToken));
        }
        else    // Try to resolve value
        {
            if(sToken.startsWith(placeholderStartToken))
            {
                value = render(sToken, context);
            }
            else if(sToken.startsWith(prefix))
            {
                if(functionStack.size() > 0)
                {
                    functionStack.peek().params.add(sToken.substring(1));
                }

                return i;
            }
            else
            {
                value = getParam(sToken, context);
            }            
        }

        if (sEnd.length() == 0 || sEnd.equals(placeholderEndToken))
        {
            // No method found but found the end of placeholder token
            if(functionStack.size() == 0)
            {
                if(value != null)
                {
                    dst.append(value);
                }
                else
                {
                    dst.append(nil);
                }
            }
            else
            {
                functionStack.peek().params.add(value);
            }
        }
        else
        {
            if(value != null)
            {
                value = value.trim();
            }

            if(end.substring(0, 1).equals("(") ||
               end.substring(0, 1).equals(marker))
            {
                // right hand side is remainder of placeholder
                StringBuffer tmp = new StringBuffer();

                end = new StringBuffer();
                i = renderTemplate(src, tmp, nil, i, endTokens, end, context);                
            }

            if(end.substring(0, 1).equals(")"))
            {
                if ( functionStack.size() > 0 )
                {
                    // Pop method out of stack to invoke
                    MethodExpressionDescriptor descriptor = functionStack.pop();

                    if(functionStack.size() > 0 )
                    {
                        functionStack.peek().params.add(invokeMethodEvaluator(descriptor.function, descriptor.params.toArray(new String[0]), context));
                    }
                    else
                    {
                        dst.append(invokeMethodEvaluator(descriptor.function, descriptor.params.toArray(new String[0]), context));
                    }

                    end = new StringBuffer();
                    StringBuffer tmp = new StringBuffer();
                    i = renderTemplate(src, tmp, nil, i, endTokens, end, context);
                }
            }
        }

        return i;
    }

    protected String invokeMethodEvaluator(String method, String[] params, RenderContext... context){

        StringBuffer result = new StringBuffer();

        result.append("[ ")
              .append(method)
              .append(" ( ");

        if(params != null)
        {
            for(int i=0; i<params.length; i++)
            {
                result.append(params[i]);

                if(i != params.length-1)
                {
                    result.append(" , ");
                }
            }
        }

        result.append(" ) ")
              .append(" ] ");

        return result.toString();
    }

}
skaffman
  • 398,947
  • 96
  • 818
  • 769
Barcelona
  • 1
  • 1