View Javadoc
1   package org.apache.maven.shared.filtering;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *    http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.FilterReader;
24  import java.io.IOException;
25  import java.io.Reader;
26  import java.util.LinkedHashSet;
27  import java.util.Set;
28  
29  import org.codehaus.plexus.interpolation.InterpolationException;
30  import org.codehaus.plexus.interpolation.Interpolator;
31  import org.codehaus.plexus.interpolation.RecursionInterceptor;
32  import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
33  import org.codehaus.plexus.interpolation.multi.DelimiterSpecification;
34  
35  /**
36   * A FilterReader implementation, that works with Interpolator interface instead of it's own interpolation
37   * implementation. This implementation is heavily based on org.codehaus.plexus.util.InterpolationFilterReader.
38   *
39   * @author cstamas
40   * @author Olivier Lamy
41   *
42   * @since 1.0
43   */
44  public class MultiDelimiterInterpolatorFilterReaderLineEnding
45      extends FilterReader
46  {
47  
48      /**
49       * Interpolator used to interpolate
50       */
51      private Interpolator interpolator;
52  
53      private RecursionInterceptor recursionInterceptor;
54  
55      /**
56       * replacement text from a token
57       */
58      private String replaceData = null;
59  
60      /**
61       * Index into replacement data
62       */
63      private int replaceIndex = 0;
64  
65      /**
66       * Default begin token.
67       */
68      public static final String DEFAULT_BEGIN_TOKEN = "${";
69  
70      /**
71       * Default end token.
72       */
73      public static final String DEFAULT_END_TOKEN = "}";
74  
75      /**
76       * true by default to preserve backward comp
77       */
78      private boolean interpolateWithPrefixPattern = true;
79  
80      private String escapeString;
81  
82      private boolean useEscape = false;
83  
84      /**
85       * if true escapeString will be preserved \{foo} -> \{foo}
86       */
87      private boolean preserveEscapeString = false;
88  
89      private LinkedHashSet<DelimiterSpecification> delimiters = new LinkedHashSet<DelimiterSpecification>();
90  
91      private String beginToken;
92  
93      private String endToken;
94  
95      private boolean supportMultiLineFiltering;
96  
97      /**
98       * must always be bigger than escape string plus delimiters, but doesn't need to be exact
99       */
100     private int markLength = 128;
101 
102     private static final int maxBufferSize = 8192;
103 
104     private boolean eof = false;
105 
106     /**
107      * This constructor uses default begin token ${ and default end token }.
108      *
109      * @param in                        reader to use
110      * @param interpolator              interpolator instance to use
111      * @param supportMultiLineFiltering If multi line filtering is allowed
112      */
113     public MultiDelimiterInterpolatorFilterReaderLineEnding( Reader in, Interpolator interpolator,
114                                                              boolean supportMultiLineFiltering )
115     {
116         this( in, interpolator, new SimpleRecursionInterceptor(), supportMultiLineFiltering );
117     }
118 
119     /**
120      * @param in                        reader to use
121      * @param interpolator              interpolator instance to use
122      * @param ri                        The {@link RecursionInterceptor} to use to prevent recursive expressions.
123      * @param supportMultiLineFiltering If multi line filtering is allowed
124      */
125     public MultiDelimiterInterpolatorFilterReaderLineEnding( Reader in, Interpolator interpolator,
126                                                              RecursionInterceptor ri,
127                                                              boolean supportMultiLineFiltering )
128     {
129         // wrap our own buffer, so we can use mark/reset safely.
130         super( new BufferedReader( in, maxBufferSize ) );
131 
132         this.interpolator = interpolator;
133 
134         // always cache answers, since we'll be sending in pure expressions, not mixed text.
135         this.interpolator.setCacheAnswers( true );
136 
137         recursionInterceptor = ri;
138 
139         delimiters.add( DelimiterSpecification.DEFAULT_SPEC );
140 
141         this.supportMultiLineFiltering = supportMultiLineFiltering;
142 
143         calculateMarkLength();
144 
145     }
146 
147     public boolean removeDelimiterSpec( String delimiterSpec )
148     {
149         return delimiters.remove( DelimiterSpecification.parse( delimiterSpec ) );
150     }
151 
152     public MultiDelimiterInterpolatorFilterReaderLineEnding setDelimiterSpecs( Set<String> specs )
153     {
154         delimiters.clear();
155         for ( String spec : specs )
156         {
157             delimiters.add( DelimiterSpecification.parse( spec ) );
158             markLength += spec.length() * 2;
159         }
160 
161         return this;
162     }
163 
164     /**
165      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
166      * the stream is reached.
167      *
168      * @param n The number of characters to skip
169      * @return the number of characters actually skipped
170      * @throws IllegalArgumentException If <code>n</code> is negative.
171      * @throws IOException              If an I/O error occurs
172      */
173     public long skip( long n )
174         throws IOException
175     {
176         if ( n < 0L )
177         {
178             throw new IllegalArgumentException( "skip value is negative" );
179         }
180 
181         for ( long i = 0; i < n; i++ )
182         {
183             if ( read() == -1 )
184             {
185                 return i;
186             }
187         }
188         return n;
189     }
190 
191     /**
192      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
193      * occurs, or the end of the stream is reached.
194      *
195      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
196      * @param off  Offset at which to start storing characters.
197      * @param len  Maximum number of characters to read.
198      * @return the number of characters read, or -1 if the end of the stream has been reached
199      * @throws IOException If an I/O error occurs
200      */
201     public int read( char cbuf[], int off, int len )
202         throws IOException
203     {
204         for ( int i = 0; i < len; i++ )
205         {
206             int ch = read();
207             if ( ch == -1 )
208             {
209                 if ( i == 0 )
210                 {
211                     return -1;
212                 }
213                 else
214                 {
215                     return i;
216                 }
217             }
218             cbuf[off + i] = (char) ch;
219         }
220         return len;
221     }
222 
223     /**
224      * Returns the next character in the filtered stream, replacing tokens from the original stream.
225      *
226      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
227      * @throws IOException if the underlying stream throws an IOException during reading
228      */
229     public int read()
230         throws IOException
231     {
232         if ( replaceIndex > 0 )
233         {
234             return replaceData.charAt( replaceData.length() - ( replaceIndex-- ) );
235         }
236         if ( eof )
237         {
238             return -1;
239         }
240 
241         BoundedReader in = new BoundedReader(this.in, markLength);
242 
243         int ch = in.read();
244         if ( ( ch == -1 ) || ( ch == '\n' && !supportMultiLineFiltering ) )
245         {
246             return ch;
247         }
248 
249         boolean inEscape = ( useEscape && ch == escapeString.charAt( 0 ) );
250 
251         StringBuffer key = new StringBuffer();
252 
253         // have we found an escape string?
254         if ( inEscape )
255         {
256             for ( int i = 0; i < escapeString.length(); i++ )
257             {
258                 key.append( (char) ch );
259 
260                 if ( ch != escapeString.charAt( i ) || ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
261                 {
262                     // mismatch, EOF or EOL, no escape string here
263                     in.reset();
264                     inEscape = false;
265                     key.setLength( 0 );
266                     break;
267                 }
268 
269                 ch = in.read();
270 
271             }
272 
273         }
274 
275         // have we found a delimiter?
276         int max = 0;
277         for ( DelimiterSpecification spec : delimiters )
278         {
279             String begin = spec.getBegin();
280 
281             // longest match wins
282             if ( begin.length() < max )
283             {
284                 continue;
285             }
286 
287             for ( int i = 0; i < begin.length(); i++ )
288             {
289                 if ( ch != begin.charAt( i ) || ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
290                 {
291                     // mismatch, EOF or EOL, no match
292                     break;
293                 }
294 
295                 if ( i == begin.length() - 1 )
296                 {
297 
298                     beginToken = spec.getBegin();
299                     endToken = spec.getEnd();
300 
301                 }
302 
303                 ch = in.read();
304 
305             }
306 
307             in.reset();
308             in.skip( key.length() );
309             ch = in.read();
310 
311         }
312 
313         // escape means no luck, prevent parsing of the escaped character, and return
314         if ( inEscape )
315         {
316 
317             if ( beginToken != null )
318             {
319                 if ( !preserveEscapeString )
320                 {
321                     key.setLength( 0 );
322                 }
323             }
324 
325             beginToken = null;
326             endToken = null;
327 
328             key.append( (char) ch );
329 
330             replaceData = key.toString();
331             replaceIndex = key.length();
332 
333             return read();
334 
335         }
336 
337         // no match means no luck, reset and return
338         if ( beginToken == null || beginToken.length() == 0 || endToken == null || endToken.length() == 0 )
339         {
340 
341             in.reset();
342             return in.read();
343 
344         }
345 
346         // we're committed, find the end token, EOL or EOF
347 
348         key.append(beginToken);
349         in.reset();
350         in.skip( beginToken.length() );
351         ch = in.read();
352 
353         int endTokenSize = endToken.length();
354         int end = endTokenSize;
355         do
356         {
357             if ( ch == -1 )
358             {
359                 break;
360             }
361             else if ( ch == '\n' && !supportMultiLineFiltering )
362             {
363                 // EOL
364                 key.append( (char) ch );
365                 break;
366             }
367 
368             key.append( (char) ch );
369 
370             if ( ch == this.endToken.charAt( endTokenSize - end ) )
371             {
372                 end--;
373                 if ( end == 0 )
374                 {
375                     break;
376                 }
377             }
378             else
379             {
380                 end = endTokenSize;
381             }
382 
383             ch = in.read();
384         }
385         while ( true );
386 
387         // reset back to no tokens
388         beginToken = null;
389         endToken = null;
390 
391         // found endtoken? interpolate our key resolved above
392         String value = null;
393         if ( end == 0 )
394         {
395             try
396             {
397                 if ( interpolateWithPrefixPattern )
398                 {
399                     value = interpolator.interpolate( key.toString(), "", recursionInterceptor );
400                 }
401                 else
402                 {
403                     value = interpolator.interpolate( key.toString(), recursionInterceptor );
404                 }
405             }
406             catch ( InterpolationException e )
407             {
408                 IllegalArgumentException error = new IllegalArgumentException( e.getMessage() );
409                 error.initCause( e );
410 
411                 throw error;
412             }
413         }
414         else
415         {
416             // no endtoken? Write current char and continue in search for next expression
417             in.reset();
418             return in.read();
419         }
420 
421         // write away the value if present, otherwise the key unmodified
422         if ( value != null )
423         {
424             replaceData = value;
425             replaceIndex = value.length();
426         }
427         else
428         {
429             replaceData = key.toString();
430             replaceIndex = key.length();
431         }
432 
433         if ( ch == -1 )
434         {
435             eof = true;
436         }
437         return read();
438 
439     }
440 
441     public boolean isInterpolateWithPrefixPattern()
442     {
443         return interpolateWithPrefixPattern;
444     }
445 
446     public void setInterpolateWithPrefixPattern( boolean interpolateWithPrefixPattern )
447     {
448         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
449     }
450 
451     public String getEscapeString()
452     {
453         return escapeString;
454     }
455 
456     public void setEscapeString( String escapeString )
457     {
458         // TODO NPE if escapeString is null ?
459         if ( escapeString != null && escapeString.length() >= 1 )
460         {
461             this.escapeString = escapeString;
462             this.useEscape = escapeString != null && escapeString.length() >= 1;
463             calculateMarkLength();
464         }
465     }
466 
467     public boolean isPreserveEscapeString()
468     {
469         return preserveEscapeString;
470     }
471 
472     public void setPreserveEscapeString( boolean preserveEscapeString )
473     {
474         this.preserveEscapeString = preserveEscapeString;
475     }
476 
477     public RecursionInterceptor getRecursionInterceptor()
478     {
479         return recursionInterceptor;
480     }
481 
482     public MultiDelimiterInterpolatorFilterReaderLineEnding setRecursionInterceptor(
483         RecursionInterceptor recursionInterceptor )
484     {
485         this.recursionInterceptor = recursionInterceptor;
486         return this;
487     }
488 
489     private void calculateMarkLength()
490     {
491         markLength = 128;
492 
493         if ( escapeString != null )
494         {
495 
496             markLength += escapeString.length();
497 
498         }
499         for ( DelimiterSpecification spec : delimiters )
500         {
501             markLength += spec.getBegin().length();
502             markLength += spec.getEnd().length();
503 
504         }
505     }
506 
507 }