All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.mule.util.TemplateParser Maven / Gradle / Ivy

There is a newer version: 3.9.0
Show newest version
/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * TemplateParser is a simple string parser that will substitute
 * tokens in a string with values supplied in a Map.
 */
public final class TemplateParser
{
    public static final String ANT_TEMPLATE_STYLE = "ant";
    public static final String SQUARE_TEMPLATE_STYLE = "square";
    public static final String CURLY_TEMPLATE_STYLE = "curly";
    public static final String WIGGLY_MULE_TEMPLATE_STYLE = "mule";

    private static final String DOLLAR_ESCAPE = "@@@";
    private static final String NULL_AS_STRING = "null";

    private static final Map patterns = new HashMap();

    static
    {
        patterns.put(ANT_TEMPLATE_STYLE, new PatternInfo(ANT_TEMPLATE_STYLE, "\\$\\{[^\\{\\}]+\\}", "${", "}"));
        patterns.put(SQUARE_TEMPLATE_STYLE, new PatternInfo(SQUARE_TEMPLATE_STYLE, "\\[[^\\[\\]]+\\]", "[", "]"));
        patterns.put(CURLY_TEMPLATE_STYLE, new PatternInfo(CURLY_TEMPLATE_STYLE, "\\{[^\\{\\}}]+\\}", "{", "}"));

        // Such a complex regex is needed to support nested expressions, otherwise we
        // have to do this manually or using an ANTLR grammar etc.

        // Support for 6 levels (5 nested)
        patterns.put(WIGGLY_MULE_TEMPLATE_STYLE, new PatternInfo(WIGGLY_MULE_TEMPLATE_STYLE,
        "#\\[((?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[.*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?)\\]", "#[", "]"));
    }

    /**
     * logger used by this class
     */
    protected static final Log logger = LogFactory.getLog(TemplateParser.class);

    public static final Pattern ANT_TEMPLATE_PATTERN = patterns.get(ANT_TEMPLATE_STYLE).getPattern();
    public static final Pattern SQUARE_TEMPLATE_PATTERN = patterns.get(SQUARE_TEMPLATE_STYLE).getPattern();
    public static final Pattern CURLY_TEMPLATE_PATTERN = patterns.get(CURLY_TEMPLATE_STYLE).getPattern();
    public static final Pattern WIGGLY_MULE_TEMPLATE_PATTERN = patterns.get(WIGGLY_MULE_TEMPLATE_STYLE).getPattern();

    private final Pattern pattern;
    private final int pre;
    private final int post;
    private final PatternInfo style;


    public static TemplateParser createAntStyleParser()
    {
        return new TemplateParser(ANT_TEMPLATE_STYLE);
    }

    public static TemplateParser createSquareBracesStyleParser()
    {
        return new TemplateParser(SQUARE_TEMPLATE_STYLE);
    }

    public static TemplateParser createCurlyBracesStyleParser()
    {
        return new TemplateParser(CURLY_TEMPLATE_STYLE);
    }

    public static TemplateParser createMuleStyleParser()
    {
        return new TemplateParser(WIGGLY_MULE_TEMPLATE_STYLE);
    }

    private TemplateParser(String styleName)
    {
        this.style = patterns.get(styleName);
        if (this.style == null)
        {
            throw new IllegalArgumentException("Unknown template style: " + styleName);

        }
        pattern = style.getPattern();
        pre = style.getPrefix().length();
        post = style.getSuffix().length();
    }

    /**
     * Matches one or more templates against a Map of key value pairs. If a value for
     * a template is not found in the map the template is left as is in the return
     * String
     *
     * @param props    the key/value pairs to match against
     * @param template the string containing the template place holders i.e. My name
     *                 is ${name}
     * @return the parsed String
     */
    public String parse(Map props, String template)
    {
        return parse(props, template, null);
    }

    /**
     * Matches one or more templates against a Map of key value pairs. If a value for
     * a template is not found in the map the template is left as is in the return
     * String
     *
     * @param callback a callback used to resolve the property name
     * @param template the string containing the template place holders i.e. My name
     *                 is ${name}
     * @return the parsed String
     */
    public String parse(TemplateCallback callback, String template)
    {
        return parse(null, template, callback);
    }

    protected String parse(Map props, String template, TemplateCallback callback)
    {
        String result = template;
        Map newProps = props;
        if (props != null && !(props instanceof CaseInsensitiveHashMap))
        {
            newProps = new CaseInsensitiveHashMap(props);
        }

        Matcher m = pattern.matcher(result);

        while (m.find())
        {
            Object value = null;

            String match = m.group();
            String propname = match.substring(pre, match.length() - post);

            if (callback != null)
            {
                value = callback.match(propname);
                if (value == null)
                {
                    value = NULL_AS_STRING;
                }
            }
            else if (newProps != null)
            {
                value = newProps.get(propname);
            }

            if (value == null)
            {
                if (logger.isDebugEnabled())
                {
                    logger.debug("Value " + propname + " not found in context");
                }
            }
            else
            {
                String matchRegex = Pattern.quote(match);
                String valueString = value.toString();
                //need to escape $ as they resolve into group references, escaping them was not enough
                //This smells a bit like a hack, but one way or another these characters need to be escaped
                if (valueString.indexOf('$') != -1)
                {
                    valueString = valueString.replaceAll("\\$", DOLLAR_ESCAPE);
                }

                if (valueString.indexOf('\\') != -1)
                {
                    valueString = valueString.replaceAll("\\\\", "\\\\\\\\");
                }

                result = result.replaceAll(matchRegex, valueString);
            }
        }
        if (result.indexOf(DOLLAR_ESCAPE) != -1)
        {
            result = result.replaceAll(DOLLAR_ESCAPE, "\\$");
        }
        return result;
    }

    /**
     * Matches one or more templates against a Map of key value pairs. If a value for
     * a template is not found in the map the template is left as is in the return
     * String
     *
     * @param props     the key/value pairs to match against
     * @param templates A List of templates
     * @return the parsed String
     */
    public List parse(Map props, List templates)
    {
        if (templates == null)
        {
            return new ArrayList();
        }

        List list = new ArrayList(templates.size());
        for (Object tmpl : templates)
        {
            list.add(parse(props, tmpl.toString()));
        }
        return list;
    }

    /**
     * Matches one or more templates against a Map of key value pairs. If a value for
     * a template is not found in the map the template is left as is in the return
     * String
     *
     * @param props     the key/value pairs to match against
     * @param templates A Map of templates. The values for each map entry will be
     *                  parsed
     * @return the parsed String
     */
    public Map parse(final Map props, Map templates)
    {
        return parse(new TemplateCallback()
        {
            @Override
            public Object match(String token)
            {
                return props.get(token);
            }
        }, templates);
    }

    public Map parse(TemplateCallback callback, Map templates)
    {
        if (templates == null)
        {
            return new HashMap();
        }

        Map map = new HashMap(templates.size());
        for (Map.Entry entry : templates.entrySet())
        {
            map.put(entry.getKey(), parse(callback, entry.getValue().toString()));
        }
        return map;
    }

    public PatternInfo getStyle()
    {
        return style;
    }

    public boolean isContainsTemplate(String value)
    {
        if (value == null)
        {
            return false;
        }

        Matcher m = pattern.matcher(value);
        return m.find();
    }

    public boolean isValid(String expression)
    {
        try
        {
            style.validate(expression);
            return true;
        }
        catch (IllegalArgumentException e)
        {
            return false;
        }
    }

    public void validate(String expression) throws IllegalArgumentException
    {
        style.validate(expression);
    }

    public static interface TemplateCallback
    {
        Object match(String token);
    }


    public static class PatternInfo
    {
        String name;
        String regEx;
        String prefix;
        String suffix;

        PatternInfo(String name, String regEx, String prefix, String suffix)
        {
            this.name = name;
            this.regEx = regEx;
            if (prefix.length() < 1 || prefix.length() > 2)
            {
                throw new IllegalArgumentException("Prefix can only be one or two characters long: " + prefix);
            }
            this.prefix = prefix;
            if (suffix.length() != 1)
            {
                throw new IllegalArgumentException("Suffix can only be one character long: " + suffix);
            }
            this.suffix = suffix;
        }

        public String getRegEx()
        {
            return regEx;
        }

        public String getPrefix()
        {
            return prefix;
        }

        public String getSuffix()
        {
            return suffix;
        }

        public String getName()
        {
            return name;
        }

        public Pattern getPattern()
        {
            return Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
        }

        public void validate(String expression) throws IllegalArgumentException
        {
            String currentExpression = expression;
            int lastMatchIdx = 0;
            while (lastMatchIdx < expression.length())
            {
                int start = currentExpression.indexOf(prefix);
                if (start == -1)
                {
                    //no more expressions to validate
                    break;
                }
                lastMatchIdx += start;
                currentExpression = currentExpression.substring(start);
                Matcher m = getPattern().matcher(currentExpression);
                boolean found = m.find();
                if (found)
                {
                    if (!currentExpression.startsWith(m.group()))
                    {
                        throw new IllegalArgumentException("Invalid Expression");
                    }
                    int matchSize = m.group().length();
                    lastMatchIdx += matchSize;
                    currentExpression = currentExpression.substring(matchSize);
                }
                else
                {
                    throw new IllegalArgumentException("Invalid Expression");
                }
            }
        }

    }
}