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 = 16;
101 
102     private boolean eof = false;
103 
104     /**
105      * This constructor uses default begin token ${ and default end token }.
106      *
107      * @param in                        reader to use
108      * @param interpolator              interpolator instance to use
109      * @param supportMultiLineFiltering If multi line filtering is allowed
110      */
111     public MultiDelimiterInterpolatorFilterReaderLineEnding( Reader in, Interpolator interpolator,
112                                                              boolean supportMultiLineFiltering )
113     {
114         this( in, interpolator, new SimpleRecursionInterceptor(), supportMultiLineFiltering );
115     }
116 
117     /**
118      * @param in                        reader to use
119      * @param interpolator              interpolator instance to use
120      * @param ri                        The {@link RecursionInterceptor} to use to prevent recursive expressions.
121      * @param supportMultiLineFiltering If multi line filtering is allowed
122      */
123     public MultiDelimiterInterpolatorFilterReaderLineEnding( Reader in, Interpolator interpolator,
124                                                              RecursionInterceptor ri,
125                                                              boolean supportMultiLineFiltering )
126     {
127         // wrap our own buffer, so we can use mark/reset safely.
128         super( new BufferedReader( in ) );
129 
130         this.interpolator = interpolator;
131 
132         // always cache answers, since we'll be sending in pure expressions, not mixed text.
133         this.interpolator.setCacheAnswers( true );
134 
135         recursionInterceptor = ri;
136 
137         delimiters.add( DelimiterSpecification.DEFAULT_SPEC );
138 
139         this.supportMultiLineFiltering = supportMultiLineFiltering;
140 
141         calculateMarkLength();
142 
143     }
144 
145 
146     public boolean removeDelimiterSpec( String delimiterSpec )
147     {
148         return delimiters.remove( DelimiterSpecification.parse( delimiterSpec ) );
149     }
150 
151     public MultiDelimiterInterpolatorFilterReaderLineEnding setDelimiterSpecs( Set<String> specs )
152     {
153         delimiters.clear();
154         for ( String spec : specs )
155         {
156             delimiters.add( DelimiterSpecification.parse( spec ) );
157             markLength += spec.length() * 2;
158         }
159 
160         return this;
161     }
162 
163     /**
164      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
165      * the stream is reached.
166      *
167      * @param n The number of characters to skip
168      * @return the number of characters actually skipped
169      * @throws IllegalArgumentException If <code>n</code> is negative.
170      * @throws IOException              If an I/O error occurs
171      */
172     public long skip( long n )
173         throws IOException
174     {
175         if ( n < 0L )
176         {
177             throw new IllegalArgumentException( "skip value is negative" );
178         }
179 
180         for ( long i = 0; i < n; i++ )
181         {
182             if ( read() == -1 )
183             {
184                 return i;
185             }
186         }
187         return n;
188     }
189 
190     /**
191      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
192      * occurs, or the end of the stream is reached.
193      *
194      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
195      * @param off  Offset at which to start storing characters.
196      * @param len  Maximum number of characters to read.
197      * @return the number of characters read, or -1 if the end of the stream has been reached
198      * @throws IOException If an I/O error occurs
199      */
200     public int read( char cbuf[], int off, int len )
201         throws IOException
202     {
203         for ( int i = 0; i < len; i++ )
204         {
205             int ch = read();
206             if ( ch == -1 )
207             {
208                 if ( i == 0 )
209                 {
210                     return -1;
211                 }
212                 else
213                 {
214                     return i;
215                 }
216             }
217             cbuf[off + i] = (char) ch;
218         }
219         return len;
220     }
221 
222     /**
223      * Returns the next character in the filtered stream, replacing tokens from the original stream.
224      *
225      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
226      * @throws IOException if the underlying stream throws an IOException during reading
227      */
228     public int read()
229         throws IOException
230     {
231         if ( replaceIndex > 0 )
232         {
233             return replaceData.charAt( replaceData.length() - ( replaceIndex-- ) );
234         }
235         if ( eof )
236         {
237             return -1;
238         }
239 
240         in.mark( markLength );
241 
242         int ch = in.read();
243         if ( ( ch == -1 ) || ( ch == '\n' && !supportMultiLineFiltering ) )
244         {
245             return ch;
246         }
247 
248         boolean inEscape = ( useEscape && ch == escapeString.charAt( 0 ) );
249 
250         StringBuffer key = new StringBuffer();
251 
252         // have we found an escape string?
253         if ( inEscape )
254         {
255             for ( int i = 0; i < escapeString.length(); i++ )
256             {
257                 key.append( (char) ch );
258 
259                 if ( ch != escapeString.charAt( i ) || ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
260                 {
261                     // mismatch, EOF or EOL, no escape string here
262                     in.reset();
263                     inEscape = false;
264                     key.setLength( 0 );
265                     break;
266                 }
267 
268                 ch = in.read();
269 
270             }
271 
272         }
273 
274         // have we found a delimiter?
275         int max = 0;
276         for ( DelimiterSpecification spec : delimiters )
277         {
278             String begin = spec.getBegin();
279 
280             // longest match wins
281             if ( begin.length() < max )
282             {
283                 continue;
284             }
285 
286             for ( int i = 0; i < begin.length(); i++ )
287             {
288                 if ( ch != begin.charAt( i ) || ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
289                 {
290                     // mismatch, EOF or EOL, no match
291                     break;
292                 }
293 
294                 if ( i == begin.length() - 1 )
295                 {
296 
297                     beginToken = spec.getBegin();
298                     endToken = spec.getEnd();
299 
300                 }
301 
302                 ch = in.read();
303 
304             }
305 
306             in.reset();
307             in.skip( key.length() );
308             ch = in.read();
309 
310         }
311 
312         // escape means no luck, prevent parsing of the escaped character, and return
313         if ( inEscape )
314         {
315 
316             if ( beginToken != null )
317             {
318                 if ( !preserveEscapeString )
319                 {
320                     key.setLength( 0 );
321                 }
322             }
323 
324             beginToken = null;
325             endToken = null;
326 
327             key.append( (char) ch );
328 
329             replaceData = key.toString();
330             replaceIndex = key.length();
331 
332             return read();
333 
334         }
335 
336         // no match means no luck, reset and return
337         if ( beginToken == null || beginToken.length() == 0 || endToken == null || endToken.length() == 0 )
338         {
339 
340             in.reset();
341             return in.read();
342 
343         }
344 
345         // we're committed, find the end token, EOL or EOF
346 
347         key.append( beginToken );
348         in.reset();
349         in.skip( beginToken.length() );
350         ch = in.read();
351 
352         int endTokenSize = endToken.length();
353         int end = endTokenSize;
354         do
355         {
356             if ( ch == -1 )
357             {
358                 break;
359             }
360             else if ( ch == '\n' && !supportMultiLineFiltering )
361             {
362                 // EOL
363                 key.append( (char) ch );
364                 break;
365             }
366 
367             key.append( (char) ch );
368 
369             if ( ch == this.endToken.charAt( endTokenSize - end ) )
370             {
371                 end--;
372                 if ( end == 0 )
373                 {
374                     break;
375                 }
376             }
377             else
378             {
379                 end = endTokenSize;
380             }
381 
382             ch = in.read();
383         }
384         while ( true );
385 
386         // reset back to no tokens
387         beginToken = null;
388         endToken = null;
389 
390         // found endtoken? interpolate our key resolved above
391         String value = null;
392         if ( end == 0 )
393         {
394             try
395             {
396                 if ( interpolateWithPrefixPattern )
397                 {
398                     value = interpolator.interpolate( key.toString(), "", recursionInterceptor );
399                 }
400                 else
401                 {
402                     value = interpolator.interpolate( key.toString(), recursionInterceptor );
403                 }
404             }
405             catch ( InterpolationException e )
406             {
407                 IllegalArgumentException error = new IllegalArgumentException( e.getMessage() );
408                 error.initCause( e );
409 
410                 throw error;
411             }
412         }
413         else
414         {
415             // no endtoken? Write current char and continue in search for next expression
416             in.reset();
417             return in.read();
418         }
419 
420         // write away the value if present, otherwise the key unmodified
421         if ( value != null )
422         {
423             replaceData = value;
424             replaceIndex = value.length();
425         }
426         else
427         {
428             replaceData = key.toString();
429             replaceIndex = key.length();
430         }
431 
432         if ( ch == -1 )
433         {
434             eof = true;
435         }
436         return read();
437 
438     }
439 
440     public boolean isInterpolateWithPrefixPattern()
441     {
442         return interpolateWithPrefixPattern;
443     }
444 
445     public void setInterpolateWithPrefixPattern( boolean interpolateWithPrefixPattern )
446     {
447         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
448     }
449 
450     public String getEscapeString()
451     {
452         return escapeString;
453     }
454 
455     public void setEscapeString( String escapeString )
456     {
457         // TODO NPE if escapeString is null ?
458         if ( escapeString != null && escapeString.length() >= 1 )
459         {
460             this.escapeString = escapeString;
461             this.useEscape = escapeString != null && escapeString.length() >= 1;
462             calculateMarkLength();
463         }
464     }
465 
466     public boolean isPreserveEscapeString()
467     {
468         return preserveEscapeString;
469     }
470 
471     public void setPreserveEscapeString( boolean preserveEscapeString )
472     {
473         this.preserveEscapeString = preserveEscapeString;
474     }
475 
476     public RecursionInterceptor getRecursionInterceptor()
477     {
478         return recursionInterceptor;
479     }
480 
481     public MultiDelimiterInterpolatorFilterReaderLineEnding setRecursionInterceptor(
482         RecursionInterceptor recursionInterceptor )
483     {
484         this.recursionInterceptor = recursionInterceptor;
485         return this;
486     }
487 
488     private void calculateMarkLength()
489     {
490         markLength = 16;
491 
492         if ( escapeString != null )
493         {
494 
495             markLength += escapeString.length();
496 
497         }
498         for ( DelimiterSpecification spec : delimiters )
499         {
500             markLength += spec.getBegin().length();
501             markLength += spec.getEnd().length();
502 
503         }
504     }
505 
506 }