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