View Javadoc

1   /*
2    * $Id: TemplateParser.java 20441 2010-12-02 16:15:09Z dfeist $
3    * --------------------------------------------------------------------------------------
4    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
5    *
6    * The software in this package is published under the terms of the CPAL v1.0
7    * license, a copy of which has been included with this distribution in the
8    * LICENSE.txt file.
9    */
10  
11  package org.mule.util;
12  
13  import java.util.ArrayList;
14  import java.util.HashMap;
15  import java.util.Iterator;
16  import java.util.List;
17  import java.util.Map;
18  import java.util.Stack;
19  import java.util.regex.Matcher;
20  import java.util.regex.Pattern;
21  
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  
25  /**
26   * <code>TemplateParser</code> is a simple string parser that will substitute
27   * tokens in a string with values supplied in a Map.
28   */
29  public final class TemplateParser
30  {
31      public static final String ANT_TEMPLATE_STYLE = "ant";
32      public static final String SQUARE_TEMPLATE_STYLE = "square";
33      public static final String CURLY_TEMPLATE_STYLE = "curly";
34      public static final String WIGGLY_MULE_TEMPLATE_STYLE = "mule";
35  
36      private static final String DOLLAR_ESCAPE = "@@@";
37  
38      private static final Map<String, PatternInfo> patterns = new HashMap<String, PatternInfo>();
39  
40      static
41      {
42          patterns.put(ANT_TEMPLATE_STYLE, new PatternInfo(ANT_TEMPLATE_STYLE, "\\$\\{[^\\}]+\\}", "${", "}"));
43          patterns.put(SQUARE_TEMPLATE_STYLE, new PatternInfo(SQUARE_TEMPLATE_STYLE, "\\[[^\\]]+\\]", "[", "]"));
44          patterns.put(CURLY_TEMPLATE_STYLE, new PatternInfo(CURLY_TEMPLATE_STYLE, "\\{[^\\}]+\\}", "{", "}"));
45  
46          // Such a complex regex is needed to support nested expressions, otherwise we
47          // have to do this manually or using an ANTLR grammar etc.
48  
49          // Support for 3 levels (2 nested)
50          patterns.put(WIGGLY_MULE_TEMPLATE_STYLE, new PatternInfo(WIGGLY_MULE_TEMPLATE_STYLE,
51              "#\\[((?:#\\[(?:#\\[.*?\\]|\\[.*?\\]|.)*?\\]|\\[.*?\\]|.)*?)\\]", "#[", "]"));
52  
53          // Support for 2 levels (1 nested)
54          // "#\\[((?:#\\[.*?\\]|\\[.*?\\]|.)*?)\\]"
55      }
56  
57      /**
58       * logger used by this class
59       */
60      protected static final Log logger = LogFactory.getLog(TemplateParser.class);
61  
62      public static final Pattern ANT_TEMPLATE_PATTERN = patterns.get(ANT_TEMPLATE_STYLE).getPattern();
63      public static final Pattern SQUARE_TEMPLATE_PATTERN = patterns.get(SQUARE_TEMPLATE_STYLE).getPattern();
64      public static final Pattern CURLY_TEMPLATE_PATTERN = patterns.get(CURLY_TEMPLATE_STYLE).getPattern();
65      public static final Pattern WIGGLY_MULE_TEMPLATE_PATTERN = patterns.get(WIGGLY_MULE_TEMPLATE_STYLE).getPattern();
66  
67      private final Pattern pattern;
68      private final int pre;
69      private final int post;
70      private final PatternInfo style;
71  
72  
73      public static TemplateParser createAntStyleParser()
74      {
75          return new TemplateParser(ANT_TEMPLATE_STYLE);
76      }
77  
78      public static TemplateParser createSquareBracesStyleParser()
79      {
80          return new TemplateParser(SQUARE_TEMPLATE_STYLE);
81      }
82  
83      public static TemplateParser createCurlyBracesStyleParser()
84      {
85          return new TemplateParser(CURLY_TEMPLATE_STYLE);
86      }
87  
88      public static TemplateParser createMuleStyleParser()
89      {
90          return new TemplateParser(WIGGLY_MULE_TEMPLATE_STYLE);
91      }
92  
93      private TemplateParser(String styleName)
94      {
95          this.style = patterns.get(styleName);
96          if (this.style == null)
97          {
98              throw new IllegalArgumentException("Unknown template style: " + styleName);
99  
100         }
101         pattern = style.getPattern();
102         pre = style.getPrefix().length();
103         post = style.getSuffix().length();
104     }
105 
106     /**
107      * Matches one or more templates against a Map of key value pairs. If a value for
108      * a template is not found in the map the template is left as is in the return
109      * String
110      *
111      * @param props    the key/value pairs to match against
112      * @param template the string containing the template place holders i.e. My name
113      *                 is ${name}
114      * @return the parsed String
115      */
116     public String parse(Map props, String template)
117     {
118         return parse(props, template, null);
119     }
120 
121     /**
122      * Matches one or more templates against a Map of key value pairs. If a value for
123      * a template is not found in the map the template is left as is in the return
124      * String
125      *
126      * @param callback a callback used to resolve the property name
127      * @param template the string containing the template place holders i.e. My name
128      *                 is ${name}
129      * @return the parsed String
130      */
131     public String parse(TemplateCallback callback, String template)
132     {
133         return parse(null, template, callback);
134     }
135 
136     protected String parse(Map props, String template, TemplateCallback callback)
137     {
138         String result = template;
139         Map newProps = props;
140         if (props != null && !(props instanceof CaseInsensitiveHashMap))
141         {
142             newProps = new CaseInsensitiveHashMap(props);
143         }
144 
145         Matcher m = pattern.matcher(result);
146 
147         while (m.find())
148         {
149             Object value = null;
150 
151             String match = m.group();
152             String propname = match.substring(pre, match.length() - post);
153 
154             if (callback != null)
155             {
156                 value = callback.match(propname);
157             }
158             else if (newProps != null)
159             {
160                 value = newProps.get(propname);
161             }
162 
163             if (value == null)
164             {
165                 if (logger.isDebugEnabled())
166                 {
167                     logger.debug("Value " + propname + " not found in context");
168                 }
169             }
170             else
171             {
172                 String matchRegex = escape(match);
173                 String valueString = value.toString();
174                 //need to escape $ as they resolve into group references, escaping them was not enough
175                 //This smells a bit like a hack, but one way or another these characters need to be escaped
176                 if (valueString.indexOf('$') != -1)
177                 {
178                     valueString = valueString.replaceAll("\\$", DOLLAR_ESCAPE);
179                 }
180 
181                 if (valueString.indexOf('\\') != -1)
182                 {
183                     valueString = valueString.replaceAll("\\\\", "\\\\\\\\");
184                 }
185 
186                 result = result.replaceAll(matchRegex, valueString);
187             }
188         }
189         if (result.indexOf(DOLLAR_ESCAPE) != -1)
190         {
191             result = result.replaceAll(DOLLAR_ESCAPE, "\\$");
192         }
193         return result;
194     }
195 
196     /**
197      * Matches one or more templates against a Map of key value pairs. If a value for
198      * a template is not found in the map the template is left as is in the return
199      * String
200      *
201      * @param props     the key/value pairs to match against
202      * @param templates A List of templates
203      * @return the parsed String
204      */
205     public List parse(Map props, List templates)
206     {
207         if (templates == null)
208         {
209             return new ArrayList();
210         }
211         List list = new ArrayList(templates.size());
212         for (Iterator iterator = templates.iterator(); iterator.hasNext();)
213         {
214             list.add(parse(props, iterator.next().toString()));
215         }
216         return list;
217     }
218 
219     /**
220      * Matches one or more templates against a Map of key value pairs. If a value for
221      * a template is not found in the map the template is left as is in the return
222      * String
223      *
224      * @param props     the key/value pairs to match against
225      * @param templates A Map of templates. The values for each map entry will be
226      *                  parsed
227      * @return the parsed String
228      */
229     public Map parse(final Map props, Map templates)
230     {
231         return parse(new TemplateCallback()
232         {
233             public Object match(String token)
234             {
235                 return props.get(token);
236             }
237         }, templates);
238     }
239 
240     public Map parse(TemplateCallback callback, Map templates)
241     {
242         if (templates == null)
243         {
244             return new HashMap();
245         }
246         Map map = new HashMap(templates.size());
247         Map.Entry entry;
248         for (Iterator iterator = templates.entrySet().iterator(); iterator.hasNext();)
249         {
250             entry = (Map.Entry) iterator.next();
251             map.put(entry.getKey(), parse(callback, entry.getValue().toString()));
252         }
253         return map;
254     }
255 
256     private String escape(String string)
257     {
258         int length = string.length();
259         if (length == 0)
260         {
261             // nothing to do
262             return string;
263         }
264         else
265         {
266             StringBuffer buffer = new StringBuffer(length * 2);
267             for (int i = 0; i < length; i++)
268             {
269                 char currentCharacter = string.charAt(i);
270                 switch (currentCharacter)
271                 {
272                     case '[':
273                     case ']':
274                     case '{':
275                     case '}':
276                     case '(':
277                     case ')':
278                     case '$':
279                     case '#':
280                     case '*':
281                         buffer.append("\\");
282                         // fall through to append original character
283                     default:
284                         buffer.append(currentCharacter);
285                 }
286             }
287             return buffer.toString();
288         }
289     }
290 
291     public PatternInfo getStyle()
292     {
293         return style;
294     }
295 
296     public boolean isContainsTemplate(String value)
297     {
298         if (value == null)
299         {
300             return false;
301         }
302 
303         Matcher m = pattern.matcher(value);
304         return m.find();
305     }
306 
307     public boolean isValid(String expression)
308     {
309         try
310         {
311             style.validate(expression);
312             return true;
313         }
314         catch (IllegalArgumentException e)
315         {
316             return false;
317         }
318     }
319 
320     public void validate(String expression) throws IllegalArgumentException
321     {
322         style.validate(expression);
323     }
324 
325     public static interface TemplateCallback
326     {
327         Object match(String token);
328     }
329 
330 
331     public static class PatternInfo
332     {
333         String name;
334         String regEx;
335         String prefix;
336         String suffix;
337 
338         PatternInfo(String name, String regEx, String prefix, String suffix)
339         {
340             this.name = name;
341             this.regEx = regEx;
342             if (prefix.length() < 1 || prefix.length() > 2)
343             {
344                 throw new IllegalArgumentException("Prefix can only be one or two characters long: " + prefix);
345             }
346             this.prefix = prefix;
347             if (suffix.length() != 1)
348             {
349                 throw new IllegalArgumentException("Suffic can only be one character long: " + suffix);
350             }
351             this.suffix = suffix;
352         }
353 
354         public String getRegEx()
355         {
356             return regEx;
357         }
358 
359         public String getPrefix()
360         {
361             return prefix;
362         }
363 
364         public String getSuffix()
365         {
366             return suffix;
367         }
368 
369         public String getName()
370         {
371             return name;
372         }
373 
374         public Pattern getPattern()
375         {
376             return Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
377         }
378 
379         public void validate(String expression) throws IllegalArgumentException
380         {
381             Stack<Character> openDelimiterStack = new Stack<Character>();
382 
383             int charCount = expression.length();
384             int index = 0;
385             char nextChar = ' ';
386             char preDelim = 0;
387             char open;
388             char close;
389             boolean inExpression = false;
390             int expressionCount = 0;
391             if (prefix.length() == 2)
392             {
393                 preDelim = prefix.charAt(0);
394                 open = prefix.charAt(1);
395             }
396             else
397             {
398                 open = prefix.charAt(0);
399             }
400             close = suffix.charAt(0);
401 
402             for (; index < charCount; index++)
403             {
404                 nextChar = expression.charAt(index);
405                 if (preDelim != 0 && nextChar == preDelim)
406                 {
407                     //escaped
408                     if (inExpression)
409                     {
410                         if (index < charCount && expression.charAt(index + 1) == open)
411                         {
412                             throw new IllegalArgumentException(String.format("Character %s at position %s suggests an expression inside an expression", open, index));
413                         }
414                     }
415                     else if (openDelimiterStack.isEmpty())
416                     {
417                         openDelimiterStack.push(nextChar);
418                         nextChar = expression.charAt(++index);
419                         if (nextChar != open)
420                         {
421                             throw new IllegalArgumentException(String.format("Character %s at position %s must appear immediately after %s", open, index, preDelim));
422                         }
423                         inExpression = true;
424 
425                     }
426                     else
427                     {
428                         throw new IllegalArgumentException(String.format("Character %s at position %s appears out of sequence. Character cannot appear after %s", nextChar, index, openDelimiterStack.pop()));
429                     }
430                 }
431 
432                 if (nextChar == open)
433                 {
434                     if (preDelim == 0 || inExpression)
435                     {
436                         openDelimiterStack.push(nextChar);
437                     }
438                     //Check the stack size to avoid out of bounds
439                     else if (openDelimiterStack.size() == 1 && openDelimiterStack.peek().equals(preDelim))
440                     {
441                         openDelimiterStack.push(nextChar);
442                     }
443                     else
444                     {
445                         throw new IllegalArgumentException(String.format("Character %s at position %s appears out of sequence. Character cannot appear after %s", nextChar, index, preDelim));
446                     }
447                 }
448                 else if (nextChar == close)
449                 {
450                     if (openDelimiterStack.isEmpty())
451                     {
452                         throw new IllegalArgumentException(String.format("Character %s at position %s appears out of sequence", nextChar, index));
453                     }
454                     else
455                     {
456                         openDelimiterStack.pop();
457                         if (preDelim != 0 && openDelimiterStack.peek() == preDelim)
458                         {
459                             openDelimiterStack.pop();
460                         }
461 
462 
463                         if (openDelimiterStack.isEmpty())
464                         {
465                             inExpression = false;
466                             expressionCount++;
467                             //throw new IllegalArgumentException(String.format("Character %s at position %s appears out of sequence", nextChar, index));
468                         }
469                     }
470                 }
471             }
472             if (expressionCount == 0)
473             {
474                 throw new IllegalArgumentException("Not an expression: " + expression);
475             }
476         }
477 
478     }
479 }