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                     if (cacheAnswers) {
305                         existingAnswers.put(realExpr, value);
306                     }
307 
308                     matcher.reset(result);
309                 }
310             } finally {
311                 recursionInterceptor.expressionResolutionFinished(realExpr);
312             }
313         }
314 
315         return result;
316     }
317 
318     /**
319      * Return any feedback messages and errors that were generated - but
320      * suppressed - during the interpolation process. Since unresolvable
321      * expressions will be left in the source string as-is, this feedback is
322      * optional, and will only be useful for debugging interpolation problems.
323      *
324      * @return a {@link List} that may be interspersed with {@link String} and
325      * {@link Throwable} instances.
326      */
327     public List getFeedback() {
328         List messages = new ArrayList();
329         for (Object valueSource : valueSources) {
330             ValueSource vs = (ValueSource) valueSource;
331             List feedback = vs.getFeedback();
332             if (feedback != null && !feedback.isEmpty()) {
333                 messages.addAll(feedback);
334             }
335         }
336 
337         return messages;
338     }
339 
340     /**
341      * Clear the feedback messages from previous interpolate(..) calls.
342      */
343     public void clearFeedback() {
344         for (Object valueSource : valueSources) {
345             ValueSource vs = (ValueSource) valueSource;
346             vs.clearFeedback();
347         }
348     }
349 
350     /**
351      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
352      * <p>
353      * This method triggers the use of a {@link SimpleRecursionInterceptor}
354      * instance for protection against expression cycles.</p>
355      *
356      * @param input The input string to interpolate
357      *
358      * @param thisPrefixPattern An optional pattern that should be trimmed from
359      *                          the start of any expressions found in the input.
360      */
361     public String interpolate(String input, String thisPrefixPattern) throws InterpolationException {
362         return interpolate(input, thisPrefixPattern, null);
363     }
364 
365     /**
366      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
367      * <p>
368      * This method triggers the use of a {@link SimpleRecursionInterceptor}
369      * instance for protection against expression cycles. It also leaves empty the
370      * expression prefix which would otherwise be trimmed from expressions. The
371      * result is that any detected expression will be resolved as-is.</p>
372      *
373      * @param input The input string to interpolate
374      */
375     public String interpolate(String input) throws InterpolationException {
376         return interpolate(input, null, null);
377     }
378 
379     /**
380      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
381      * <p>
382      * This method leaves empty the expression prefix which would otherwise be
383      * trimmed from expressions. The result is that any detected expression will
384      * be resolved as-is.</p>
385      *
386      * @param input The input string to interpolate
387      *
388      * @param recursionInterceptor Used to protect the interpolation process
389      *                             from expression cycles, and throw an
390      *                             exception if one is detected.
391      */
392     public String interpolate(String input, RecursionInterceptor recursionInterceptor) throws InterpolationException {
393         return interpolate(input, null, recursionInterceptor);
394     }
395 
396     public boolean isReusePatterns() {
397         return reusePatterns;
398     }
399 
400     public void setReusePatterns(boolean reusePatterns) {
401         this.reusePatterns = reusePatterns;
402     }
403 
404     public boolean isCacheAnswers() {
405         return cacheAnswers;
406     }
407 
408     public void setCacheAnswers(boolean cacheAnswers) {
409         this.cacheAnswers = cacheAnswers;
410     }
411 
412     public void clearAnswers() {
413         existingAnswers.clear();
414     }
415 }