View Javadoc
1   package org.codehaus.plexus.interpolation;
2   
3   /*
4    * Copyright 2001-2008 Codehaus Foundation.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *      http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import java.util.ArrayList;
20  import java.util.HashMap;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.WeakHashMap;
24  import java.util.regex.Matcher;
25  import java.util.regex.Pattern;
26  
27  import org.codehaus.plexus.interpolation.util.StringUtils;
28  
29  /**
30   * Expansion of the original RegexBasedInterpolator, found in plexus-utils, this
31   * interpolator provides options for setting custom prefix/suffix regex parts,
32   * and includes a {@link RecursionInterceptor} parameter in its interpolate(..)
33   * call, to allow the detection of cyclical expression references.
34   *
35   */
36  public class RegexBasedInterpolator implements Interpolator {
37  
38      private String startRegex;
39  
40      private String endRegex;
41  
42      private Map existingAnswers = new HashMap();
43  
44      private List<ValueSource> valueSources = new ArrayList<ValueSource>();
45  
46      private List<InterpolationPostProcessor> postProcessors = new ArrayList<InterpolationPostProcessor>();
47  
48      private boolean reusePatterns = false;
49  
50      private boolean cacheAnswers = false;
51  
52      public static final String DEFAULT_REGEXP = "\\$\\{(.+?)\\}";
53  
54      /**
55       * the key is the regex the value is the Pattern
56       * At the class construction time the Map will contains the default Pattern
57       */
58      private Map<String, Pattern> compiledPatterns = new WeakHashMap<String, Pattern>();
59  
60      /**
61       * Setup a basic interpolator.
62       * <p><b>NOTE:</b> You will have to call</p>
63       * {@link RegexBasedInterpolator#addValueSource(ValueSource)} at least once
64       * if you use this constructor!
65       */
66      public RegexBasedInterpolator() {
67          compiledPatterns.put(DEFAULT_REGEXP, Pattern.compile(DEFAULT_REGEXP));
68      }
69  
70      /**
71       *
72       * @param reusePatterns already compiled patterns will be reused
73       */
74      public RegexBasedInterpolator(boolean reusePatterns) {
75          this();
76          this.reusePatterns = reusePatterns;
77      }
78  
79      /**
80       * Setup an interpolator with no value sources, and the specified regex pattern
81       * prefix and suffix in place of the default one.
82       * <p><b>NOTE:</b> You will have to call
83       * {@link RegexBasedInterpolator#addValueSource(ValueSource)} at least once
84       * if you use this constructor!</p>
85       *
86       * @param startRegex start of the regular expression to use
87       * @param endRegex end of the regular expression to use
88       */
89      public RegexBasedInterpolator(String startRegex, String endRegex) {
90          this();
91          this.startRegex = startRegex;
92          this.endRegex = endRegex;
93      }
94  
95      /**
96       * Setup a basic interpolator with the specified list of value sources.
97       *
98       * @param valueSources The list of value sources to use
99       */
100     public RegexBasedInterpolator(List valueSources) {
101         this();
102         this.valueSources.addAll(valueSources);
103     }
104 
105     /**
106      * Setup an interpolator with the specified value sources, and the specified
107      * regex pattern prefix and suffix in place of the default one.
108      *
109      * @param startRegex start of the regular expression to use
110      * @param endRegex end of the regular expression to use
111      * @param valueSources The list of value sources to use
112      */
113     public RegexBasedInterpolator(String startRegex, String endRegex, List valueSources) {
114         this();
115         this.startRegex = startRegex;
116         this.endRegex = endRegex;
117         this.valueSources.addAll(valueSources);
118     }
119 
120     /**
121      * {@inheritDoc}
122      */
123     public void addValueSource(ValueSource valueSource) {
124         valueSources.add(valueSource);
125     }
126 
127     /**
128      * {@inheritDoc}
129      */
130     public void removeValuesSource(ValueSource valueSource) {
131         valueSources.remove(valueSource);
132     }
133 
134     /**
135      * {@inheritDoc}
136      */
137     public void addPostProcessor(InterpolationPostProcessor postProcessor) {
138         postProcessors.add(postProcessor);
139     }
140 
141     /**
142      * {@inheritDoc}
143      */
144     public void removePostProcessor(InterpolationPostProcessor postProcessor) {
145         postProcessors.remove(postProcessor);
146     }
147 
148     /**
149      * Attempt to resolve all expressions in the given input string, using the
150      * given pattern to first trim an optional prefix from each expression. The
151      * supplied recursion interceptor will provide protection from expression
152      * cycles, ensuring that the input can be resolved or an exception is
153      * thrown.
154      *
155      * @param input The input string to interpolate
156      *
157      * @param thisPrefixPattern An optional pattern that should be trimmed from
158      *                          the start of any expressions found in the input.
159      *
160      * @param recursionInterceptor Used to protect the interpolation process
161      *                             from expression cycles, and throw an
162      *                             exception if one is detected.
163      */
164     public String interpolate(String input, String thisPrefixPattern, RecursionInterceptor recursionInterceptor)
165             throws InterpolationException {
166         if (input == null) {
167             // return empty String to prevent NPE too
168             return "";
169         }
170         if (recursionInterceptor == null) {
171             recursionInterceptor = new SimpleRecursionInterceptor();
172         }
173 
174         if (thisPrefixPattern != null && thisPrefixPattern.length() == 0) {
175             thisPrefixPattern = null;
176         }
177 
178         int realExprGroup = 2;
179         Pattern expressionPattern;
180         final String expressionDelimiterStart;
181         final String expressionDelimiterEnd;
182         if (startRegex != null || endRegex != null) {
183             if (thisPrefixPattern == null) {
184                 expressionPattern = getPattern(startRegex + endRegex);
185                 realExprGroup = 1;
186             } else {
187                 expressionPattern = getPattern(startRegex + thisPrefixPattern + endRegex);
188             }
189             expressionDelimiterStart = startRegex;
190             expressionDelimiterEnd = endRegex;
191 
192         } else {
193             expressionDelimiterStart = "${";
194             expressionDelimiterEnd = "}";
195             if (thisPrefixPattern != null) {
196                 expressionPattern = getPattern("\\$\\{(" + thisPrefixPattern + ")?(.+?)\\}");
197             } else {
198                 expressionPattern = getPattern(DEFAULT_REGEXP);
199                 realExprGroup = 1;
200             }
201         }
202         try {
203             return interpolate(
204                     input,
205                     recursionInterceptor,
206                     expressionPattern,
207                     expressionDelimiterStart,
208                     expressionDelimiterEnd,
209                     realExprGroup);
210         } finally {
211             if (!cacheAnswers) {
212                 clearAnswers();
213             }
214         }
215     }
216 
217     private Pattern getPattern(String regExp) {
218         if (!reusePatterns) {
219             return Pattern.compile(regExp);
220         }
221 
222         Pattern pattern;
223         synchronized (this) {
224             pattern = compiledPatterns.get(regExp);
225 
226             if (pattern != null) {
227                 return pattern;
228             }
229 
230             pattern = Pattern.compile(regExp);
231             compiledPatterns.put(regExp, pattern);
232         }
233 
234         return pattern;
235     }
236 
237     /**
238      * Entry point for recursive resolution of an expression and all of its
239      * nested expressions.
240      *
241      * @todo Ensure unresolvable expressions don't trigger infinite recursion.
242      */
243     private String interpolate(
244             String input,
245             RecursionInterceptor recursionInterceptor,
246             Pattern expressionPattern,
247             String expressionDelimiterStart,
248             String expressionDelimiterEnd,
249             int realExprGroup)
250             throws InterpolationException {
251         if (input == null) {
252             // return empty String to prevent NPE too
253             return "";
254         }
255         String result = input;
256 
257         Matcher matcher = expressionPattern.matcher(result);
258 
259         while (matcher.find()) {
260             String wholeExpr = matcher.group(0);
261             String realExpr = matcher.group(realExprGroup);
262 
263             if (realExpr.startsWith(".")) {
264                 realExpr = realExpr.substring(1);
265             }
266 
267             if (recursionInterceptor.hasRecursiveExpression(realExpr)) {
268                 throw new InterpolationCycleException(recursionInterceptor, realExpr, wholeExpr);
269             }
270 
271             recursionInterceptor.expressionResolutionStarted(realExpr);
272             try {
273                 Object value = existingAnswers.get(realExpr);
274                 for (ValueSource vs : valueSources) {
275                     if (value != null) break;
276 
277                     value = vs.getValue(realExpr, expressionDelimiterStart, expressionDelimiterEnd);
278                 }
279 
280                 if (value != null) {
281                     value = interpolate(
282                             String.valueOf(value),
283                             recursionInterceptor,
284                             expressionPattern,
285                             expressionDelimiterStart,
286                             expressionDelimiterEnd,
287                             realExprGroup);
288 
289                     if (postProcessors != null && !postProcessors.isEmpty()) {
290                         for (InterpolationPostProcessor postProcessor : postProcessors) {
291                             Object newVal = postProcessor.execute(realExpr, value);
292                             if (newVal != null) {
293                                 value = newVal;
294                                 break;
295                             }
296                         }
297                     }
298 
299                     // could use:
300                     // result = matcher.replaceFirst( stringValue );
301                     // but this could result in multiple lookups of stringValue, and replaceAll is not correct behaviour
302                     result = StringUtils.replace(result, wholeExpr, String.valueOf(value));
303 
304                     matcher.reset(result);
305                 }
306             } finally {
307                 recursionInterceptor.expressionResolutionFinished(realExpr);
308             }
309         }
310 
311         return result;
312     }
313 
314     /**
315      * Return any feedback messages and errors that were generated - but
316      * suppressed - during the interpolation process. Since unresolvable
317      * expressions will be left in the source string as-is, this feedback is
318      * optional, and will only be useful for debugging interpolation problems.
319      *
320      * @return a {@link List} that may be interspersed with {@link String} and
321      * {@link Throwable} instances.
322      */
323     public List getFeedback() {
324         List messages = new ArrayList();
325         for (Object valueSource : valueSources) {
326             ValueSource vs = (ValueSource) valueSource;
327             List feedback = vs.getFeedback();
328             if (feedback != null && !feedback.isEmpty()) {
329                 messages.addAll(feedback);
330             }
331         }
332 
333         return messages;
334     }
335 
336     /**
337      * Clear the feedback messages from previous interpolate(..) calls.
338      */
339     public void clearFeedback() {
340         for (Object valueSource : valueSources) {
341             ValueSource vs = (ValueSource) valueSource;
342             vs.clearFeedback();
343         }
344     }
345 
346     /**
347      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
348      * <p>
349      * This method triggers the use of a {@link SimpleRecursionInterceptor}
350      * instance for protection against expression cycles.</p>
351      *
352      * @param input The input string to interpolate
353      *
354      * @param thisPrefixPattern An optional pattern that should be trimmed from
355      *                          the start of any expressions found in the input.
356      */
357     public String interpolate(String input, String thisPrefixPattern) throws InterpolationException {
358         return interpolate(input, thisPrefixPattern, null);
359     }
360 
361     /**
362      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
363      * <p>
364      * This method triggers the use of a {@link SimpleRecursionInterceptor}
365      * instance for protection against expression cycles. It also leaves empty the
366      * expression prefix which would otherwise be trimmed from expressions. The
367      * result is that any detected expression will be resolved as-is.</p>
368      *
369      * @param input The input string to interpolate
370      */
371     public String interpolate(String input) throws InterpolationException {
372         return interpolate(input, null, null);
373     }
374 
375     /**
376      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
377      * <p>
378      * This method leaves empty the expression prefix which would otherwise be
379      * trimmed from expressions. The result is that any detected expression will
380      * be resolved as-is.</p>
381      *
382      * @param input The input string to interpolate
383      *
384      * @param recursionInterceptor Used to protect the interpolation process
385      *                             from expression cycles, and throw an
386      *                             exception if one is detected.
387      */
388     public String interpolate(String input, RecursionInterceptor recursionInterceptor) throws InterpolationException {
389         return interpolate(input, null, recursionInterceptor);
390     }
391 
392     public boolean isReusePatterns() {
393         return reusePatterns;
394     }
395 
396     public void setReusePatterns(boolean reusePatterns) {
397         this.reusePatterns = reusePatterns;
398     }
399 
400     public boolean isCacheAnswers() {
401         return cacheAnswers;
402     }
403 
404     public void setCacheAnswers(boolean cacheAnswers) {
405         this.cacheAnswers = cacheAnswers;
406     }
407 
408     public void clearAnswers() {
409         existingAnswers.clear();
410     }
411 }