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