View Javadoc
1   /*
2    * The Apache Software License, Version 1.1
3    *
4    * Copyright (c) 2002-2003 The Apache Software Foundation.  All rights
5    * reserved.
6    *
7    * Redistribution and use in source and binary forms, with or without
8    * modification, are permitted provided that the following conditions
9    * are met:
10   *
11   * 1. Redistributions of source code must retain the above copyright
12   *    notice, this list of conditions and the following disclaimer.
13   *
14   * 2. Redistributions in binary form must reproduce the above copyright
15   *    notice, this list of conditions and the following disclaimer in
16   *    the documentation and/or other materials provided with the
17   *    distribution.
18   *
19   * 3. The end-user documentation included with the redistribution, if
20   *    any, must include the following acknowlegement:
21   *       "This product includes software developed by the
22   *        Apache Software Foundation (http://www.codehaus.org/)."
23   *    Alternately, this acknowlegement may appear in the software itself,
24   *    if and wherever such third-party acknowlegements normally appear.
25   *
26   * 4. The names "Ant" and "Apache Software
27   *    Foundation" must not be used to endorse or promote products derived
28   *    from this software without prior written permission. For written
29   *    permission, please contact codehaus@codehaus.org.
30   *
31   * 5. Products derived from this software may not be called "Apache"
32   *    nor may "Apache" appear in their names without prior written
33   *    permission of the Apache Group.
34   *
35   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
36   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
39   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
42   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
44   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
45   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
46   * SUCH DAMAGE.
47   * ====================================================================
48   *
49   * This software consists of voluntary contributions made by many
50   * individuals on behalf of the Apache Software Foundation.  For more
51   * information on the Apache Software Foundation, please see
52   * <http://www.codehaus.org/>.
53   */
54  
55  package org.codehaus.plexus.interpolation;
56  
57  import java.io.FilterReader;
58  import java.io.IOException;
59  import java.io.Reader;
60  
61  /**
62   * A FilterReader implementation, that works with Interpolator interface instead of it's own interpolation
63   * implementation. This implementation is heavily based on org.codehaus.plexus.util.InterpolationFilterReader.
64   *
65   * @author cstamas
66   */
67  public class InterpolatorFilterReader
68      extends FilterReader
69  {
70  
71      /** Interpolator used to interpolate */
72      private Interpolator interpolator;
73      
74      /**
75       * @since 1.12
76       */
77      private RecursionInterceptor recursionInterceptor;
78  
79      /** replacement text from a token */
80      private String replaceData = null;
81  
82      /** Index into replacement data */
83      private int replaceIndex = -1;
84  
85      /** Index into previous data */
86      private int previousIndex = -1;
87  
88      /** Default begin token. */
89      public static final String DEFAULT_BEGIN_TOKEN = "${";
90  
91      /** Default end token. */
92      public static final String DEFAULT_END_TOKEN = "}";
93      
94      private String beginToken;
95      
96      private String orginalBeginToken;
97      
98      private String endToken;
99      
100     /** true by default to preserve backward comp */
101     private boolean interpolateWithPrefixPattern = true;
102 
103     private String escapeString;
104     
105     private boolean useEscape = false;
106     
107     /** if true escapeString will be preserved \{foo} -> \{foo} */
108     private boolean preserveEscapeString = false;
109     
110     /**
111      * this constructor use default begin token ${ and default end token } 
112      * @param in reader to use
113      * @param interpolator interpolator instance to use
114      */
115     public InterpolatorFilterReader( Reader in, Interpolator interpolator )
116     {
117         this( in, interpolator, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN );
118     }
119     
120     /**
121      * @param in reader to use
122      * @param interpolator interpolator instance to use
123      * @param beginToken start token to use
124      * @param endToken end token to use
125      */
126     public InterpolatorFilterReader( Reader in, Interpolator interpolator, String beginToken, String endToken )
127     {
128         this( in, interpolator, beginToken, endToken, new SimpleRecursionInterceptor() );
129     }    
130 
131     /**
132      * this constructor use default begin token ${ and default end token } 
133      * @param in reader to use
134      * @param interpolator interpolator instance to use
135      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
136      * @since 1.12
137      */
138     public InterpolatorFilterReader( Reader in, Interpolator interpolator, RecursionInterceptor ri )
139     {
140         this( in, interpolator, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN, ri );
141     }
142     
143     /**
144      * @param in reader to use
145      * @param interpolator interpolator instance to use
146      * @param beginToken start token to use
147      * @param endToken end token to use
148      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
149      * @since 1.12
150      */
151     public InterpolatorFilterReader( Reader in, Interpolator interpolator, String beginToken, String endToken, RecursionInterceptor ri )
152     {
153         super( in );
154 
155         this.interpolator = interpolator;
156         
157         this.beginToken = beginToken;
158         
159         this.endToken = endToken;
160         
161         recursionInterceptor = ri;
162         
163         this.orginalBeginToken = this.beginToken;
164     }    
165 
166     /**
167      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
168      * the stream is reached.
169      *
170      * @param n The number of characters to skip
171      * @return the number of characters actually skipped
172      * @exception IllegalArgumentException If <code>n</code> is negative.
173      * @exception IOException If an I/O error occurs
174      */
175     public long skip( long n )
176         throws IOException
177     {
178         if ( n < 0L )
179         {
180             throw new IllegalArgumentException( "skip value is negative" );
181         }
182 
183         for ( long i = 0; i < n; i++ )
184         {
185             if ( read() == -1 )
186             {
187                 return i;
188             }
189         }
190         return n;
191     }
192 
193     /**
194      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
195      * occurs, or the end of the stream is reached.
196      *
197      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
198      * @param off Offset at which to start storing characters.
199      * @param len Maximum number of characters to read.
200      * @return the number of characters read, or -1 if the end of the stream has been reached
201      * @exception IOException If an I/O error occurs
202      */
203     public int read( char cbuf[], int off, int len )
204         throws IOException
205     {
206         for ( int i = 0; i < len; i++ )
207         {
208             int ch = read();
209             if ( ch == -1 )
210             {
211                 if ( i == 0 )
212                 {
213                     return -1;
214                 }
215                 else
216                 {
217                     return i;
218                 }
219             }
220             cbuf[off + i] = (char) ch;
221         }
222         return len;
223     }
224 
225     /**
226      * Returns the next character in the filtered stream, replacing tokens from the original stream.
227      *
228      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
229      * @exception IOException if the underlying stream throws an IOException during reading
230      */
231     public int read()
232         throws IOException
233     {
234         if ( replaceIndex != -1 && replaceIndex < replaceData.length() )
235         {
236             int ch = replaceData.charAt( replaceIndex++ );
237             if ( replaceIndex >= replaceData.length() )
238             {
239                 replaceIndex = -1;
240             }
241             return ch;
242         }
243 
244         int ch = -1;
245         if ( previousIndex != -1 && previousIndex < this.endToken.length() )
246         {
247             ch = this.endToken.charAt( previousIndex++ );
248         }
249         else
250         {
251             ch = in.read();
252         }
253         
254         if ( ch == this.beginToken.charAt( 0 ) || ( useEscape && ch == this.orginalBeginToken.charAt( 0 ) ) )
255         {
256             StringBuilder key = new StringBuilder( );
257 
258             key.append( (char) ch );
259 
260             int beginTokenMatchPos = 1;
261 
262             do
263             {
264                 if ( previousIndex != -1 && previousIndex < this.endToken.length() )
265                 {
266                     ch = this.endToken.charAt( previousIndex++ );
267                 }
268                 else
269                 {
270                     ch = in.read();
271                 }
272                 if ( ch != -1 )
273                 {
274                     key.append( (char) ch );
275                     if ( ( beginTokenMatchPos < this.beginToken.length() )
276                         && ( ch != this.beginToken.charAt( beginTokenMatchPos++ ) )
277                         && ( useEscape && this.orginalBeginToken.length() > ( beginTokenMatchPos - 1 ) && ch != this.orginalBeginToken
278                             .charAt( beginTokenMatchPos - 1 ) ) )
279                     {
280                         ch = -1; // not really EOF but to trigger code below
281                         break;
282                     }
283                 }
284                 else
285                 {
286                     break;
287                 }
288                 // MSHARED-81 olamy : we must take care of token with length 1, escaping and same char : \@foo@
289                 // here ch == endToken == beginToken -> not going to next char : bad :-)
290                 if ( useEscape && this.orginalBeginToken == this.endToken && key.toString().startsWith( this.beginToken ) )
291                 {
292                     ch = in.read();
293                     key.append( (char) ch );
294                 }
295             }
296             while ( ch != this.endToken.charAt( 0 ) );
297 
298             // now test endToken
299             if ( ch != -1 && this.endToken.length() > 1 )
300             {
301                 int endTokenMatchPos = 1;
302 
303                 do
304                 {
305                     if ( previousIndex != -1 && previousIndex < this.endToken.length() )
306                     {
307                         ch = this.endToken.charAt( previousIndex++ );
308                     }
309                     else
310                     {
311                         ch = in.read();
312                     }
313 
314                     if ( ch != -1 )
315                     {
316                         key.append( (char) ch );
317 
318                         if ( ch != this.endToken.charAt( endTokenMatchPos++ ) )
319                         {
320                             ch = -1; // not really EOF but to trigger code below
321                             break;
322                         }
323 
324                     }
325                     else
326                     {
327                         break;
328                     }
329                 }
330                 while ( endTokenMatchPos < this.endToken.length() );
331             }
332 
333             // There is nothing left to read so we have the situation where the begin/end token
334             // are in fact the same and as there is nothing left to read we have got ourselves
335             // end of a token boundary so let it pass through.
336             if ( ch == -1 )
337             {
338                 replaceData = key.toString();
339                 replaceIndex = 1;
340                 return replaceData.charAt( 0 );
341             }
342 
343             String value = null;
344             try
345             {
346                 boolean escapeFound = false;
347                 if ( useEscape )
348                 {
349                     if ( key.toString().startsWith( escapeString + orginalBeginToken ) )
350                     {
351                         String keyStr = key.toString();
352                         if ( !preserveEscapeString )
353                         {
354                             value = keyStr.substring( escapeString.length(), keyStr.length() );
355                         }
356                         else
357                         {
358                             value = keyStr;
359                         }
360                         escapeFound = true;
361                     }
362                 }
363                 if ( !escapeFound )
364                 {
365                     if ( interpolateWithPrefixPattern )
366                     {
367                         value = interpolator.interpolate( key.toString(), "", recursionInterceptor );
368                     }
369                     else
370                     {
371                         value = interpolator.interpolate( key.toString(), recursionInterceptor );
372                     }
373                 }
374             }
375             catch ( InterpolationException e )
376             {
377                 IllegalArgumentException error = new IllegalArgumentException( e.getMessage() );
378                 error.initCause( e );
379 
380                 throw error;
381             }
382 
383             if ( value != null )
384             {
385                 if ( value.length() != 0 )
386                 {
387                     replaceData = value;
388                     replaceIndex = 0;
389                 }
390                 return read();
391             }
392             else
393             {
394                 previousIndex = 0;
395                 replaceData = key.substring( 0, key.length() - this.endToken.length() );
396                 replaceIndex = 0;
397                 return this.beginToken.charAt( 0 );
398             }
399         }
400 
401         return ch;
402     }
403 
404     public boolean isInterpolateWithPrefixPattern()
405     {
406         return interpolateWithPrefixPattern;
407     }
408 
409     public void setInterpolateWithPrefixPattern( boolean interpolateWithPrefixPattern )
410     {
411         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
412     }
413     public String getEscapeString()
414     {
415         return escapeString;
416     }
417 
418     public void setEscapeString( String escapeString )
419     {
420         // TODO NPE if escapeString is null ?
421         if ( escapeString != null && escapeString.length() >= 1 )
422         {
423             this.escapeString = escapeString;
424             this.orginalBeginToken = beginToken;
425             this.beginToken = escapeString + beginToken;
426             this.useEscape = escapeString != null && escapeString.length() >= 1;
427         }
428     }
429 
430     public boolean isPreserveEscapeString()
431     {
432         return preserveEscapeString;
433     }
434 
435     public void setPreserveEscapeString( boolean preserveEscapeString )
436     {
437         this.preserveEscapeString = preserveEscapeString;
438     }
439 
440     public RecursionInterceptor getRecursionInterceptor()
441     {
442         return recursionInterceptor;
443     }
444 
445     public InterpolatorFilterReader setRecursionInterceptor( RecursionInterceptor recursionInterceptor )
446     {
447         this.recursionInterceptor = recursionInterceptor;
448         return this;
449     }
450 }