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