View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.util;
8   
9   import java.util.ArrayList;
10  import java.util.HashMap;
11  import java.util.List;
12  import java.util.Map;
13  import java.util.Stack;
14  import java.util.regex.Matcher;
15  import java.util.regex.Pattern;
16  
17  import org.apache.commons.logging.Log;
18  import org.apache.commons.logging.LogFactory;
19  
20  /**
21   * <code>TemplateParser</code> is a simple string parser that will substitute
22   * tokens in a string with values supplied in a Map.
23   */
24  public final class TemplateParser
25  {
26      public static final String ANT_TEMPLATE_STYLE = "ant";
27      public static final String SQUARE_TEMPLATE_STYLE = "square";
28      public static final String CURLY_TEMPLATE_STYLE = "curly";
29      public static final String WIGGLY_MULE_TEMPLATE_STYLE = "mule";
30  
31      private static final String DOLLAR_ESCAPE = "@@@";
32      private static final String NULL_AS_STRING = "null";
33  
34      private static final Map<String, PatternInfo> patterns = new HashMap<String, PatternInfo>();
35  
36      static
37      {
38          patterns.put(ANT_TEMPLATE_STYLE, new PatternInfo(ANT_TEMPLATE_STYLE, "\\$\\{[^\\{\\}]+\\}", "${", "}"));
39          patterns.put(SQUARE_TEMPLATE_STYLE, new PatternInfo(SQUARE_TEMPLATE_STYLE, "\\[[^\\[\\]]+\\]", "[", "]"));
40          patterns.put(CURLY_TEMPLATE_STYLE, new PatternInfo(CURLY_TEMPLATE_STYLE, "\\{[^\\{\\}}]+\\}", "{", "}"));
41  
42          // Such a complex regex is needed to support nested expressions, otherwise we
43          // have to do this manually or using an ANTLR grammar etc.
44  
45          // Support for 6 levels (5 nested)
46          patterns.put(WIGGLY_MULE_TEMPLATE_STYLE, new PatternInfo(WIGGLY_MULE_TEMPLATE_STYLE,
47          "#\\[((?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[.*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?)\\]", "#[", "]"));
48      }
49  
50      /**
51       * logger used by this class
52       */
53      protected static final Log logger = LogFactory.getLog(TemplateParser.class);
54  
55      public static final Pattern ANT_TEMPLATE_PATTERN = patterns.get(ANT_TEMPLATE_STYLE).getPattern();
56      public static final Pattern SQUARE_TEMPLATE_PATTERN = patterns.get(SQUARE_TEMPLATE_STYLE).getPattern();
57      public static final Pattern CURLY_TEMPLATE_PATTERN = patterns.get(CURLY_TEMPLATE_STYLE).getPattern();
58      public static final Pattern WIGGLY_MULE_TEMPLATE_PATTERN = patterns.get(WIGGLY_MULE_TEMPLATE_STYLE).getPattern();
59  
60      private final Pattern pattern;
61      private final int pre;
62      private final int post;
63      private final PatternInfo style;
64  
65  
66      public static TemplateParser createAntStyleParser()
67      {
68          return new TemplateParser(ANT_TEMPLATE_STYLE);
69      }
70  
71      public static TemplateParser createSquareBracesStyleParser()
72      {
73          return new TemplateParser(SQUARE_TEMPLATE_STYLE);
74      }
75  
76      public static TemplateParser createCurlyBracesStyleParser()
77      {
78          return new TemplateParser(CURLY_TEMPLATE_STYLE);
79      }
80  
81      public static TemplateParser createMuleStyleParser()
82      {
83          return new TemplateParser(WIGGLY_MULE_TEMPLATE_STYLE);
84      }
85  
86      private TemplateParser(String styleName)
87      {
88          this.style = patterns.get(styleName);
89          if (this.style == null)
90          {
91              throw new IllegalArgumentException("Unknown template style: " + styleName);
92  
93          }
94          pattern = style.getPattern();
95          pre = style.getPrefix().length();
96          post = style.getSuffix().length();
97      }
98  
99      /**
100      * Matches one or more templates against a Map of key value pairs. If a value for
101      * a template is not found in the map the template is left as is in the return
102      * String
103      *
104      * @param props    the key/value pairs to match against
105      * @param template the string containing the template place holders i.e. My name
106      *                 is ${name}
107      * @return the parsed String
108      */
109     public String parse(Map<?, ?> props, String template)
110     {
111         return parse(props, template, null);
112     }
113 
114     /**
115      * Matches one or more templates against a Map of key value pairs. If a value for
116      * a template is not found in the map the template is left as is in the return
117      * String
118      *
119      * @param callback a callback used to resolve the property name
120      * @param template the string containing the template place holders i.e. My name
121      *                 is ${name}
122      * @return the parsed String
123      */
124     public String parse(TemplateCallback callback, String template)
125     {
126         return parse(null, template, callback);
127     }
128 
129     protected String parse(Map<?, ?> props, String template, TemplateCallback callback)
130     {
131         String result = template;
132         Map<?, ?> newProps = props;
133         if (props != null && !(props instanceof CaseInsensitiveHashMap))
134         {
135             newProps = new CaseInsensitiveHashMap(props);
136         }
137 
138         Matcher m = pattern.matcher(result);
139 
140         while (m.find())
141         {
142             Object value = null;
143 
144             String match = m.group();
145             String propname = match.substring(pre, match.length() - post);
146 
147             if (callback != null)
148             {
149                 value = callback.match(propname);
150                 if (value == null)
151                 {
152                     value = NULL_AS_STRING;
153                 }
154             }
155             else if (newProps != null)
156             {
157                 value = newProps.get(propname);
158             }
159 
160             if (value == null)
161             {
162                 if (logger.isDebugEnabled())
163                 {
164                     logger.debug("Value " + propname + " not found in context");
165                 }
166             }
167             else
168             {
169                 String matchRegex = Pattern.quote(match);
170                 String valueString = value.toString();
171                 //need to escape $ as they resolve into group references, escaping them was not enough
172                 //This smells a bit like a hack, but one way or another these characters need to be escaped
173                 if (valueString.indexOf('$') != -1)
174                 {
175                     valueString = valueString.replaceAll("\\$", DOLLAR_ESCAPE);
176                 }
177 
178                 if (valueString.indexOf('\\') != -1)
179                 {
180                     valueString = valueString.replaceAll("\\\\", "\\\\\\\\");
181                 }
182 
183                 result = result.replaceAll(matchRegex, valueString);
184             }
185         }
186         if (result.indexOf(DOLLAR_ESCAPE) != -1)
187         {
188             result = result.replaceAll(DOLLAR_ESCAPE, "\\$");
189         }
190         return result;
191     }
192 
193     /**
194      * Matches one or more templates against a Map of key value pairs. If a value for
195      * a template is not found in the map the template is left as is in the return
196      * String
197      *
198      * @param props     the key/value pairs to match against
199      * @param templates A List of templates
200      * @return the parsed String
201      */
202     public List<?> parse(Map<?, ?> props, List<?> templates)
203     {
204         if (templates == null)
205         {
206             return new ArrayList<Object>();
207         }
208 
209         List<String> list = new ArrayList<String>(templates.size());
210         for (Object tmpl : templates)
211         {
212             list.add(parse(props, tmpl.toString()));
213         }
214         return list;
215     }
216 
217     /**
218      * Matches one or more templates against a Map of key value pairs. If a value for
219      * a template is not found in the map the template is left as is in the return
220      * String
221      *
222      * @param props     the key/value pairs to match against
223      * @param templates A Map of templates. The values for each map entry will be
224      *                  parsed
225      * @return the parsed String
226      */
227     public Map<?, ?> parse(final Map<?, ?> props, Map<?, ?> templates)
228     {
229         return parse(new TemplateCallback()
230         {
231             public Object match(String token)
232             {
233                 return props.get(token);
234             }
235         }, templates);
236     }
237 
238     public Map<?, ?> parse(TemplateCallback callback, Map<?, ?> templates)
239     {
240         if (templates == null)
241         {
242             return new HashMap<Object, Object>();
243         }
244 
245         Map<Object, String> map = new HashMap<Object, String>(templates.size());
246         for (Map.Entry<?, ?> entry : templates.entrySet())
247         {
248             map.put(entry.getKey(), parse(callback, entry.getValue().toString()));
249         }
250         return map;
251     }
252 
253     public PatternInfo getStyle()
254     {
255         return style;
256     }
257 
258     public boolean isContainsTemplate(String value)
259     {
260         if (value == null)
261         {
262             return false;
263         }
264 
265         Matcher m = pattern.matcher(value);
266         return m.find();
267     }
268 
269     public boolean isValid(String expression)
270     {
271         try
272         {
273             style.validate(expression);
274             return true;
275         }
276         catch (IllegalArgumentException e)
277         {
278             return false;
279         }
280     }
281 
282     public void validate(String expression) throws IllegalArgumentException
283     {
284         style.validate(expression);
285     }
286 
287     public static interface TemplateCallback
288     {
289         Object match(String token);
290     }
291 
292 
293     public static class PatternInfo
294     {
295         String name;
296         String regEx;
297         String prefix;
298         String suffix;
299 
300         PatternInfo(String name, String regEx, String prefix, String suffix)
301         {
302             this.name = name;
303             this.regEx = regEx;
304             if (prefix.length() < 1 || prefix.length() > 2)
305             {
306                 throw new IllegalArgumentException("Prefix can only be one or two characters long: " + prefix);
307             }
308             this.prefix = prefix;
309             if (suffix.length() != 1)
310             {
311                 throw new IllegalArgumentException("Suffix can only be one character long: " + suffix);
312             }
313             this.suffix = suffix;
314         }
315 
316         public String getRegEx()
317         {
318             return regEx;
319         }
320 
321         public String getPrefix()
322         {
323             return prefix;
324         }
325 
326         public String getSuffix()
327         {
328             return suffix;
329         }
330 
331         public String getName()
332         {
333             return name;
334         }
335 
336         public Pattern getPattern()
337         {
338             return Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
339         }
340 
341         public void validate(String expression) throws IllegalArgumentException
342         {
343             String currentExpression = expression;
344             int lastMatchIdx = 0;
345             while (lastMatchIdx < expression.length())
346             {
347                 int start = currentExpression.indexOf(prefix);
348                 if (start == -1)
349                 {
350                     //no more expressions to validate
351                     break;
352                 }
353                 lastMatchIdx += start;
354                 currentExpression = currentExpression.substring(start);
355                 Matcher m = getPattern().matcher(currentExpression);
356                 boolean found = m.find();
357                 if (found)
358                 {
359                     if (!currentExpression.startsWith(m.group()))
360                     {
361                         throw new IllegalArgumentException("Invalid Expression");
362                     }
363                     int matchSize = m.group().length();
364                     lastMatchIdx += matchSize;
365                     currentExpression = currentExpression.substring(matchSize);
366                 }
367                 else
368                 {
369                     throw new IllegalArgumentException("Invalid Expression");
370                 }
371             }
372         }
373 
374     }
375 }