View Javadoc

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