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         if (startRegex != null || endRegex != null) {
181             if (thisPrefixPattern == null) {
182                 expressionPattern = getPattern(startRegex + endRegex);
183                 realExprGroup = 1;
184             } else {
185                 expressionPattern = getPattern(startRegex + thisPrefixPattern + endRegex);
186             }
187 
188         } else if (thisPrefixPattern != null) {
189             expressionPattern = getPattern("\\$\\{(" + thisPrefixPattern + ")?(.+?)\\}");
190         } else {
191             expressionPattern = getPattern(DEFAULT_REGEXP);
192             realExprGroup = 1;
193         }
194 
195         try {
196             return interpolate(input, recursionInterceptor, expressionPattern, realExprGroup);
197         } finally {
198             if (!cacheAnswers) {
199                 clearAnswers();
200             }
201         }
202     }
203 
204     private Pattern getPattern(String regExp) {
205         if (!reusePatterns) {
206             return Pattern.compile(regExp);
207         }
208 
209         Pattern pattern;
210         synchronized (this) {
211             pattern = compiledPatterns.get(regExp);
212 
213             if (pattern != null) {
214                 return pattern;
215             }
216 
217             pattern = Pattern.compile(regExp);
218             compiledPatterns.put(regExp, pattern);
219         }
220 
221         return pattern;
222     }
223 
224     /**
225      * Entry point for recursive resolution of an expression and all of its
226      * nested expressions.
227      *
228      * @todo Ensure unresolvable expressions don't trigger infinite recursion.
229      */
230     private String interpolate(
231             String input, RecursionInterceptor recursionInterceptor, Pattern expressionPattern, int realExprGroup)
232             throws InterpolationException {
233         if (input == null) {
234             // return empty String to prevent NPE too
235             return "";
236         }
237         String result = input;
238 
239         Matcher matcher = expressionPattern.matcher(result);
240 
241         while (matcher.find()) {
242             String wholeExpr = matcher.group(0);
243             String realExpr = matcher.group(realExprGroup);
244 
245             if (realExpr.startsWith(".")) {
246                 realExpr = realExpr.substring(1);
247             }
248 
249             if (recursionInterceptor.hasRecursiveExpression(realExpr)) {
250                 throw new InterpolationCycleException(recursionInterceptor, realExpr, wholeExpr);
251             }
252 
253             recursionInterceptor.expressionResolutionStarted(realExpr);
254             try {
255                 Object value = existingAnswers.get(realExpr);
256                 for (ValueSource vs : valueSources) {
257                     if (value != null) break;
258 
259                     value = vs.getValue(realExpr);
260                 }
261 
262                 if (value != null) {
263                     value = interpolate(String.valueOf(value), recursionInterceptor, expressionPattern, realExprGroup);
264 
265                     if (postProcessors != null && !postProcessors.isEmpty()) {
266                         for (InterpolationPostProcessor postProcessor : postProcessors) {
267                             Object newVal = postProcessor.execute(realExpr, value);
268                             if (newVal != null) {
269                                 value = newVal;
270                                 break;
271                             }
272                         }
273                     }
274 
275                     // could use:
276                     // result = matcher.replaceFirst( stringValue );
277                     // but this could result in multiple lookups of stringValue, and replaceAll is not correct behaviour
278                     result = StringUtils.replace(result, wholeExpr, String.valueOf(value));
279 
280                     matcher.reset(result);
281                 }
282             } finally {
283                 recursionInterceptor.expressionResolutionFinished(realExpr);
284             }
285         }
286 
287         return result;
288     }
289 
290     /**
291      * Return any feedback messages and errors that were generated - but
292      * suppressed - during the interpolation process. Since unresolvable
293      * expressions will be left in the source string as-is, this feedback is
294      * optional, and will only be useful for debugging interpolation problems.
295      *
296      * @return a {@link List} that may be interspersed with {@link String} and
297      * {@link Throwable} instances.
298      */
299     public List getFeedback() {
300         List messages = new ArrayList();
301         for (Object valueSource : valueSources) {
302             ValueSource vs = (ValueSource) valueSource;
303             List feedback = vs.getFeedback();
304             if (feedback != null && !feedback.isEmpty()) {
305                 messages.addAll(feedback);
306             }
307         }
308 
309         return messages;
310     }
311 
312     /**
313      * Clear the feedback messages from previous interpolate(..) calls.
314      */
315     public void clearFeedback() {
316         for (Object valueSource : valueSources) {
317             ValueSource vs = (ValueSource) valueSource;
318             vs.clearFeedback();
319         }
320     }
321 
322     /**
323      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
324      * <p>
325      * This method triggers the use of a {@link SimpleRecursionInterceptor}
326      * instance for protection against expression cycles.</p>
327      *
328      * @param input The input string to interpolate
329      *
330      * @param thisPrefixPattern An optional pattern that should be trimmed from
331      *                          the start of any expressions found in the input.
332      */
333     public String interpolate(String input, String thisPrefixPattern) throws InterpolationException {
334         return interpolate(input, thisPrefixPattern, null);
335     }
336 
337     /**
338      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
339      * <p>
340      * This method triggers the use of a {@link SimpleRecursionInterceptor}
341      * instance for protection against expression cycles. It also leaves empty the
342      * expression prefix which would otherwise be trimmed from expressions. The
343      * result is that any detected expression will be resolved as-is.</p>
344      *
345      * @param input The input string to interpolate
346      */
347     public String interpolate(String input) throws InterpolationException {
348         return interpolate(input, null, null);
349     }
350 
351     /**
352      * See {@link RegexBasedInterpolator#interpolate(String, String, RecursionInterceptor)}.
353      * <p>
354      * This method leaves empty the expression prefix which would otherwise be
355      * trimmed from expressions. The result is that any detected expression will
356      * be resolved as-is.</p>
357      *
358      * @param input The input string to interpolate
359      *
360      * @param recursionInterceptor Used to protect the interpolation process
361      *                             from expression cycles, and throw an
362      *                             exception if one is detected.
363      */
364     public String interpolate(String input, RecursionInterceptor recursionInterceptor) throws InterpolationException {
365         return interpolate(input, null, recursionInterceptor);
366     }
367 
368     public boolean isReusePatterns() {
369         return reusePatterns;
370     }
371 
372     public void setReusePatterns(boolean reusePatterns) {
373         this.reusePatterns = reusePatterns;
374     }
375 
376     public boolean isCacheAnswers() {
377         return cacheAnswers;
378     }
379 
380     public void setCacheAnswers(boolean cacheAnswers) {
381         this.cacheAnswers = cacheAnswers;
382     }
383 
384     public void clearAnswers() {
385         existingAnswers.clear();
386     }
387 }