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.multi;
56  
57  import org.codehaus.plexus.interpolation.InterpolationException;
58  import org.codehaus.plexus.interpolation.Interpolator;
59  import org.codehaus.plexus.interpolation.RecursionInterceptor;
60  import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
61  
62  import java.io.FilterReader;
63  import java.io.IOException;
64  import java.io.Reader;
65  import java.util.LinkedHashSet;
66  
67  /**
68   * A FilterReader implementation, that works with Interpolator interface instead of it's own interpolation
69   * implementation. This implementation is heavily based on org.codehaus.plexus.util.InterpolationFilterReader.
70   *
71   * @author cstamas
72   */
73  public class MultiDelimiterInterpolatorFilterReader
74      extends FilterReader
75  {
76  
77      /** Interpolator used to interpolate */
78      private Interpolator interpolator;
79      
80      /**
81       * @since 1.12
82       */
83      private RecursionInterceptor recursionInterceptor;
84  
85      /** replacement text from a token */
86      private String replaceData = null;
87  
88      /** Index into replacement data */
89      private int replaceIndex = -1;
90  
91      /** Index into previous data */
92      private int previousIndex = -1;
93  
94      /** Default begin token. */
95      public static final String DEFAULT_BEGIN_TOKEN = "${";
96  
97      /** Default end token. */
98      public static final String DEFAULT_END_TOKEN = "}";
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     private LinkedHashSet<DelimiterSpecification> delimiters = new LinkedHashSet<DelimiterSpecification>();
111     
112     private DelimiterSpecification currentSpec;
113 
114     private String beginToken;
115 
116     private String originalBeginToken;
117 
118     private String endToken;
119     
120     /**
121      * this constructor use default begin token ${ and default end token } 
122      * @param in reader to use
123      * @param interpolator interpolator instance to use
124      */
125     public MultiDelimiterInterpolatorFilterReader( Reader in, Interpolator interpolator )
126     {
127         this( in, interpolator, new SimpleRecursionInterceptor() );
128     }
129     
130     /**
131      * @param in reader to use
132      * @param interpolator interpolator instance to use
133      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
134      * @since 1.12
135      */
136     public MultiDelimiterInterpolatorFilterReader( Reader in, Interpolator interpolator, RecursionInterceptor ri )
137     {
138         super( in );
139 
140         this.interpolator = interpolator;
141         
142         // always cache answers, since we'll be sending in pure expressions, not mixed text.
143         this.interpolator.setCacheAnswers( true );
144         
145         recursionInterceptor = ri;
146         
147         delimiters.add( DelimiterSpecification.DEFAULT_SPEC );
148     }    
149 
150     public MultiDelimiterInterpolatorFilterReader addDelimiterSpec( String delimiterSpec )
151     {
152         if ( delimiterSpec == null )
153         {
154             return this;
155         }        
156         delimiters.add( DelimiterSpecification.parse( delimiterSpec ) );
157         return this;
158     }
159     
160     public boolean removeDelimiterSpec( String delimiterSpec )
161     {
162         if ( delimiterSpec == null )
163         {
164             return false;
165         }        
166         return delimiters.remove( DelimiterSpecification.parse( delimiterSpec ) );
167     }
168     
169     public MultiDelimiterInterpolatorFilterReader setDelimiterSpecs( LinkedHashSet<String> specs )
170     {
171         delimiters.clear();
172         for ( String spec : specs )
173         {
174             if ( spec == null )
175             {
176                 continue;
177             }
178             delimiters.add( DelimiterSpecification.parse( spec ) );
179         }
180         
181         return this;
182     }
183     
184     /**
185      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
186      * the stream is reached.
187      *
188      * @param n The number of characters to skip
189      * @return the number of characters actually skipped
190      * @exception IllegalArgumentException If <code>n</code> is negative.
191      * @exception IOException If an I/O error occurs
192      */
193     public long skip( long n )
194         throws IOException
195     {
196         if ( n < 0L )
197         {
198             throw new IllegalArgumentException( "skip value is negative" );
199         }
200 
201         for ( long i = 0; i < n; i++ )
202         {
203             if ( read() == -1 )
204             {
205                 return i;
206             }
207         }
208         return n;
209     }
210 
211     /**
212      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
213      * occurs, or the end of the stream is reached.
214      *
215      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
216      * @param off Offset at which to start storing characters.
217      * @param len Maximum number of characters to read.
218      * @return the number of characters read, or -1 if the end of the stream has been reached
219      * @exception IOException If an I/O error occurs
220      */
221     public int read( char cbuf[], int off, int len )
222         throws IOException
223     {
224         for ( int i = 0; i < len; i++ )
225         {
226             int ch = read();
227             if ( ch == -1 )
228             {
229                 if ( i == 0 )
230                 {
231                     return -1;
232                 }
233                 else
234                 {
235                     return i;
236                 }
237             }
238             cbuf[off + i] = (char) ch;
239         }
240         return len;
241     }
242 
243     /**
244      * Returns the next character in the filtered stream, replacing tokens from the original stream.
245      *
246      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
247      * @exception IOException if the underlying stream throws an IOException during reading
248      */
249     public int read()
250         throws IOException
251     {
252         if ( replaceIndex != -1 && replaceIndex < replaceData.length() )
253         {
254             int ch = replaceData.charAt( replaceIndex++ );
255             if ( replaceIndex >= replaceData.length() )
256             {
257                 replaceIndex = -1;
258             }
259             return ch;
260         }
261 
262         int ch = -1;
263         if ( previousIndex != -1 && previousIndex < this.endToken.length() )
264         {
265             ch = this.endToken.charAt( previousIndex++ );
266         }
267         else
268         {
269             ch = in.read();
270         }
271         
272         boolean inEscape = false;
273         
274         if ( ( inEscape = ( useEscape && ch == escapeString.charAt( 0 ) ) ) || reselectDelimiterSpec( ch ) )
275         {
276             StringBuilder key = new StringBuilder( );
277 
278             key.append( (char) ch );
279             
280             // this will happen when we're using an escape string, and ONLY then.
281             boolean atEnd = false;
282 
283             if ( inEscape )
284             {
285                 for( int i = 0; i < escapeString.length() - 1; i++ )
286                 {
287                     ch = in.read();
288                     if ( ch == -1 )
289                     {
290                         atEnd = true;
291                         break;
292                     }
293                     
294                     key.append( (char) ch );
295                 }
296                 
297                 if ( !atEnd )
298                 {
299                     ch = in.read();
300                     if ( !reselectDelimiterSpec( ch ) )
301                     {
302                         replaceData = key.toString();
303                         replaceIndex = 1;
304                         return replaceData.charAt( 0 );
305                     }
306                     else
307                     {
308                         key.append( (char) ch );
309                     }
310                 }
311             }
312 
313             int beginTokenMatchPos = 1;
314             do
315             {
316                 if ( atEnd )
317                 {
318                     // didn't finish reading the escape string.
319                     break;
320                 }
321                 
322                 if ( previousIndex != -1 && previousIndex < this.endToken.length() )
323                 {
324                     ch = this.endToken.charAt( previousIndex++ );
325                 }
326                 else
327                 {
328                     ch = in.read();
329                 }
330                 if ( ch != -1 )
331                 {
332                     key.append( (char) ch );
333                     if ( ( beginTokenMatchPos < this.originalBeginToken.length() )
334                         && ( ch != this.originalBeginToken.charAt( beginTokenMatchPos ) )  )
335                     {
336                         ch = -1; // not really EOF but to trigger code below
337                         break;
338                     }
339                 }
340                 else
341                 {
342                     break;
343                 }
344                 
345                 beginTokenMatchPos++;
346             }
347             while ( ch != this.endToken.charAt( 0 ) );
348 
349             // now test endToken
350             if ( ch != -1 && this.endToken.length() > 1 )
351             {
352                 int endTokenMatchPos = 1;
353 
354                 do
355                 {
356                     if ( previousIndex != -1 && previousIndex < this.endToken.length() )
357                     {
358                         ch = this.endToken.charAt( previousIndex++ );
359                     }
360                     else
361                     {
362                         ch = in.read();
363                     }
364 
365                     if ( ch != -1 )
366                     {
367                         key.append( (char) ch );
368 
369                         if ( ch != this.endToken.charAt( endTokenMatchPos++ ) )
370                         {
371                             ch = -1; // not really EOF but to trigger code below
372                             break;
373                         }
374 
375                     }
376                     else
377                     {
378                         break;
379                     }
380                 }
381                 while ( endTokenMatchPos < this.endToken.length() );
382             }
383 
384             // There is nothing left to read so we have the situation where the begin/end token
385             // are in fact the same and as there is nothing left to read we have got ourselves
386             // end of a token boundary so let it pass through.
387             if ( ch == -1 )
388             {
389                 replaceData = key.toString();
390                 replaceIndex = 1;
391                 return replaceData.charAt( 0 );
392             }
393 
394             String value = null;
395             try
396             {
397                 boolean escapeFound = false;
398                 if ( useEscape )
399                 {
400                     if ( key.toString().startsWith( beginToken ) )
401                     {
402                         String keyStr = key.toString();
403                         if ( !preserveEscapeString )
404                         {
405                             value = keyStr.substring( escapeString.length(), keyStr.length() );
406                         }
407                         else
408                         {
409                             value = keyStr;
410                         }
411                         escapeFound = true;
412                     }
413                 }
414                 if ( !escapeFound )
415                 {
416                     if ( interpolateWithPrefixPattern )
417                     {
418                         value = interpolator.interpolate( key.toString(), "", recursionInterceptor );
419                     }
420                     else
421                     {
422                         value = interpolator.interpolate( key.toString(), recursionInterceptor );
423                     }
424                 }
425             }
426             catch ( InterpolationException e )
427             {
428                 IllegalArgumentException error = new IllegalArgumentException( e.getMessage() );
429                 error.initCause( e );
430 
431                 throw error;
432             }
433 
434             if ( value != null )
435             {
436                 if ( value.length() != 0 )
437                 {
438                     replaceData = value;
439                     replaceIndex = 0;
440                 }
441                 return read();
442             }
443             else
444             {
445                 previousIndex = 0;
446                 replaceData = key.substring( 0, key.length() - this.endToken.length() );
447                 replaceIndex = 0;
448                 return this.beginToken.charAt( 0 );
449             }
450         }
451 
452         return ch;
453     }
454 
455     private boolean reselectDelimiterSpec( int ch )
456     {
457         for ( DelimiterSpecification spec : delimiters )
458         {
459             if ( ch == spec.getBegin().charAt( 0 ) )
460             {
461                 currentSpec = spec;
462                 originalBeginToken = currentSpec.getBegin();
463                 beginToken = useEscape ? escapeString + originalBeginToken : originalBeginToken;
464                 endToken = currentSpec.getEnd();
465 
466                 return true;
467             }
468         }
469         
470         return false;
471     }
472 
473     public boolean isInterpolateWithPrefixPattern()
474     {
475         return interpolateWithPrefixPattern;
476     }
477 
478     public void setInterpolateWithPrefixPattern( boolean interpolateWithPrefixPattern )
479     {
480         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
481     }
482     public String getEscapeString()
483     {
484         return escapeString;
485     }
486 
487     public void setEscapeString( String escapeString )
488     {
489         // TODO NPE if escapeString is null ?
490         if ( escapeString != null && escapeString.length() >= 1 )
491         {
492             this.escapeString = escapeString;
493             this.useEscape = escapeString != null && escapeString.length() >= 1;
494         }
495     }
496 
497     public boolean isPreserveEscapeString()
498     {
499         return preserveEscapeString;
500     }
501 
502     public void setPreserveEscapeString( boolean preserveEscapeString )
503     {
504         this.preserveEscapeString = preserveEscapeString;
505     }
506 
507     public RecursionInterceptor getRecursionInterceptor()
508     {
509         return recursionInterceptor;
510     }
511 
512     public MultiDelimiterInterpolatorFilterReader setRecursionInterceptor( RecursionInterceptor recursionInterceptor )
513     {
514         this.recursionInterceptor = recursionInterceptor;
515         return this;
516     }
517 }