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   * @version $Id$
67   */
68  public class InterpolatorFilterReader
69      extends FilterReader
70  {
71  
72      /** Interpolator used to interpolate */
73      private Interpolator interpolator;
74      
75      /**
76       * @since 1.12
77       */
78      private RecursionInterceptor recursionInterceptor;
79  
80      /** replacement text from a token */
81      private String replaceData = null;
82  
83      /** Index into replacement data */
84      private int replaceIndex = -1;
85  
86      /** Index into previous data */
87      private int previousIndex = -1;
88  
89      /** Default begin token. */
90      public static final String DEFAULT_BEGIN_TOKEN = "${";
91  
92      /** Default end token. */
93      public static final String DEFAULT_END_TOKEN = "}";
94      
95      private String beginToken;
96      
97      private String orginalBeginToken;
98      
99      private String endToken;
100     
101     /** true by default to preserve backward comp */
102     private boolean interpolateWithPrefixPattern = true;
103 
104     private String escapeString;
105     
106     private boolean useEscape = false;
107     
108     /** if true escapeString will be preserved \{foo} -> \{foo} */
109     private boolean preserveEscapeString = false;
110     
111     /**
112      * this constructor use default begin token ${ and default end token } 
113      * @param in reader to use
114      * @param interpolator interpolator instance to use
115      */
116     public InterpolatorFilterReader( Reader in, Interpolator interpolator )
117     {
118         this( in, interpolator, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN );
119     }
120     
121     /**
122      * @param in reader to use
123      * @param interpolator interpolator instance to use
124      * @param beginToken start token to use
125      * @param endToken end token to use
126      */
127     public InterpolatorFilterReader( Reader in, Interpolator interpolator, String beginToken, String endToken )
128     {
129         this( in, interpolator, beginToken, endToken, new SimpleRecursionInterceptor() );
130     }    
131 
132     /**
133      * this constructor use default begin token ${ and default end token } 
134      * @param in reader to use
135      * @param interpolator interpolator instance to use
136      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
137      * @since 1.12
138      */
139     public InterpolatorFilterReader( Reader in, Interpolator interpolator, RecursionInterceptor ri )
140     {
141         this( in, interpolator, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN, new SimpleRecursionInterceptor() );
142     }
143     
144     /**
145      * @param in reader to use
146      * @param interpolator interpolator instance to use
147      * @param beginToken start token to use
148      * @param endToken end token to use
149      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
150      * @since 1.12
151      */
152     public InterpolatorFilterReader( Reader in, Interpolator interpolator, String beginToken, String endToken, RecursionInterceptor ri )
153     {
154         super( in );
155 
156         this.interpolator = interpolator;
157         
158         this.beginToken = beginToken;
159         
160         this.endToken = endToken;
161         
162         recursionInterceptor = ri;
163         
164         this.orginalBeginToken = this.beginToken;
165     }    
166 
167     /**
168      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
169      * the stream is reached.
170      *
171      * @param n The number of characters to skip
172      * @return the number of characters actually skipped
173      * @exception IllegalArgumentException If <code>n</code> is negative.
174      * @exception IOException If an I/O error occurs
175      */
176     public long skip( long n )
177         throws IOException
178     {
179         if ( n < 0L )
180         {
181             throw new IllegalArgumentException( "skip value is negative" );
182         }
183 
184         for ( long i = 0; i < n; i++ )
185         {
186             if ( read() == -1 )
187             {
188                 return i;
189             }
190         }
191         return n;
192     }
193 
194     /**
195      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
196      * occurs, or the end of the stream is reached.
197      *
198      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
199      * @param off Offset at which to start storing characters.
200      * @param len Maximum number of characters to read.
201      * @return the number of characters read, or -1 if the end of the stream has been reached
202      * @exception IOException If an I/O error occurs
203      */
204     public int read( char cbuf[], int off, int len )
205         throws IOException
206     {
207         for ( int i = 0; i < len; i++ )
208         {
209             int ch = read();
210             if ( ch == -1 )
211             {
212                 if ( i == 0 )
213                 {
214                     return -1;
215                 }
216                 else
217                 {
218                     return i;
219                 }
220             }
221             cbuf[off + i] = (char) ch;
222         }
223         return len;
224     }
225 
226     /**
227      * Returns the next character in the filtered stream, replacing tokens from the original stream.
228      *
229      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
230      * @exception IOException if the underlying stream throws an IOException during reading
231      */
232     public int read()
233         throws IOException
234     {
235         if ( replaceIndex != -1 && replaceIndex < replaceData.length() )
236         {
237             int ch = replaceData.charAt( replaceIndex++ );
238             if ( replaceIndex >= replaceData.length() )
239             {
240                 replaceIndex = -1;
241             }
242             return ch;
243         }
244 
245         int ch = -1;
246         if ( previousIndex != -1 && previousIndex < this.endToken.length() )
247         {
248             ch = this.endToken.charAt( previousIndex++ );
249         }
250         else
251         {
252             ch = in.read();
253         }
254         
255         if ( ch == this.beginToken.charAt( 0 ) || ( useEscape && ch == this.orginalBeginToken.charAt( 0 ) ) )
256         {
257             StringBuilder key = new StringBuilder( );
258 
259             key.append( (char) ch );
260 
261             int beginTokenMatchPos = 1;
262 
263             do
264             {
265                 if ( previousIndex != -1 && previousIndex < this.endToken.length() )
266                 {
267                     ch = this.endToken.charAt( previousIndex++ );
268                 }
269                 else
270                 {
271                     ch = in.read();
272                 }
273                 if ( ch != -1 )
274                 {
275                     key.append( (char) ch );
276                     if ( ( beginTokenMatchPos < this.beginToken.length() )
277                         && ( ch != this.beginToken.charAt( beginTokenMatchPos++ ) )
278                         && ( useEscape && this.orginalBeginToken.length() > ( beginTokenMatchPos - 1 ) && ch != this.orginalBeginToken
279                             .charAt( beginTokenMatchPos - 1 ) ) )
280                     {
281                         ch = -1; // not really EOF but to trigger code below
282                         break;
283                     }
284                 }
285                 else
286                 {
287                     break;
288                 }
289                 // MSHARED-81 olamy : we must take care of token with length 1, escaping and same char : \@foo@
290                 // here ch == endToken == beginToken -> not going to next char : bad :-)
291                 if ( useEscape && this.orginalBeginToken == this.endToken && key.toString().startsWith( this.beginToken ) )
292                 {
293                     ch = in.read();
294                     key.append( (char) ch );
295                 }
296             }
297             while ( ch != this.endToken.charAt( 0 ) );
298 
299             // now test endToken
300             if ( ch != -1 && this.endToken.length() > 1 )
301             {
302                 int endTokenMatchPos = 1;
303 
304                 do
305                 {
306                     if ( previousIndex != -1 && previousIndex < this.endToken.length() )
307                     {
308                         ch = this.endToken.charAt( previousIndex++ );
309                     }
310                     else
311                     {
312                         ch = in.read();
313                     }
314 
315                     if ( ch != -1 )
316                     {
317                         key.append( (char) ch );
318 
319                         if ( ch != this.endToken.charAt( endTokenMatchPos++ ) )
320                         {
321                             ch = -1; // not really EOF but to trigger code below
322                             break;
323                         }
324 
325                     }
326                     else
327                     {
328                         break;
329                     }
330                 }
331                 while ( endTokenMatchPos < this.endToken.length() );
332             }
333 
334             // There is nothing left to read so we have the situation where the begin/end token
335             // are in fact the same and as there is nothing left to read we have got ourselves
336             // end of a token boundary so let it pass through.
337             if ( ch == -1 )
338             {
339                 replaceData = key.toString();
340                 replaceIndex = 1;
341                 return replaceData.charAt( 0 );
342             }
343 
344             String value = null;
345             try
346             {
347                 boolean escapeFound = false;
348                 if ( useEscape )
349                 {
350                     if ( key.toString().startsWith( escapeString + orginalBeginToken ) )
351                     {
352                         String keyStr = key.toString();
353                         if ( !preserveEscapeString )
354                         {
355                             value = keyStr.substring( escapeString.length(), keyStr.length() );
356                         }
357                         else
358                         {
359                             value = keyStr;
360                         }
361                         escapeFound = true;
362                     }
363                 }
364                 if ( !escapeFound )
365                 {
366                     if ( interpolateWithPrefixPattern )
367                     {
368                         value = interpolator.interpolate( key.toString(), "", recursionInterceptor );
369                     }
370                     else
371                     {
372                         value = interpolator.interpolate( key.toString(), recursionInterceptor );
373                     }
374                 }
375             }
376             catch ( InterpolationException e )
377             {
378                 IllegalArgumentException error = new IllegalArgumentException( e.getMessage() );
379                 error.initCause( e );
380 
381                 throw error;
382             }
383 
384             if ( value != null )
385             {
386                 if ( value.length() != 0 )
387                 {
388                     replaceData = value;
389                     replaceIndex = 0;
390                 }
391                 return read();
392             }
393             else
394             {
395                 previousIndex = 0;
396                 replaceData = key.substring( 0, key.length() - this.endToken.length() );
397                 replaceIndex = 0;
398                 return this.beginToken.charAt( 0 );
399             }
400         }
401 
402         return ch;
403     }
404 
405     public boolean isInterpolateWithPrefixPattern()
406     {
407         return interpolateWithPrefixPattern;
408     }
409 
410     public void setInterpolateWithPrefixPattern( boolean interpolateWithPrefixPattern )
411     {
412         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
413     }
414     public String getEscapeString()
415     {
416         return escapeString;
417     }
418 
419     public void setEscapeString( String escapeString )
420     {
421         // TODO NPE if escapeString is null ?
422         if ( escapeString != null && escapeString.length() >= 1 )
423         {
424             this.escapeString = escapeString;
425             this.orginalBeginToken = beginToken;
426             this.beginToken = escapeString + beginToken;
427             this.useEscape = escapeString != null && escapeString.length() >= 1;
428         }
429     }
430 
431     public boolean isPreserveEscapeString()
432     {
433         return preserveEscapeString;
434     }
435 
436     public void setPreserveEscapeString( boolean preserveEscapeString )
437     {
438         this.preserveEscapeString = preserveEscapeString;
439     }
440 
441     public RecursionInterceptor getRecursionInterceptor()
442     {
443         return recursionInterceptor;
444     }
445 
446     public InterpolatorFilterReader setRecursionInterceptor( RecursionInterceptor recursionInterceptor )
447     {
448         this.recursionInterceptor = recursionInterceptor;
449         return this;
450     }
451 }