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 it's 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 public long skip( long n )
163 throws IOException, IllegalArgumentException
164 {
165 if ( n < 0L )
166 {
167 throw new IllegalArgumentException( "skip value is negative" );
168 }
169
170 for ( long i = 0; i < n; i++ )
171 {
172 if ( read() == -1 )
173 {
174 return i;
175 }
176 }
177 return n;
178 }
179
180 /**
181 * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
182 * occurs, or the end of the stream is reached.
183 *
184 * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
185 * @param off Offset at which to start storing characters.
186 * @param len Maximum number of characters to read.
187 * @return the number of characters read, or -1 if the end of the stream has been reached
188 * @throws IOException If an I/O error occurs
189 */
190 public int read( char cbuf[], int off, int len )
191 throws IOException
192 {
193 for ( int i = 0; i < len; i++ )
194 {
195 int ch = read();
196 if ( ch == -1 )
197 {
198 if ( i == 0 )
199 {
200 return -1;
201 }
202 else
203 {
204 return i;
205 }
206 }
207 cbuf[off + i] = (char) ch;
208 }
209 return len;
210 }
211
212 /**
213 * Returns the next character in the filtered stream, replacing tokens from the original stream.
214 *
215 * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
216 * @throws IOException if the underlying stream throws an IOException during reading
217 */
218 public int read()
219 throws IOException
220 {
221 if ( replaceIndex > 0 )
222 {
223 return replaceData.charAt( replaceData.length() - ( replaceIndex-- ) );
224 }
225 if ( eof )
226 {
227 return -1;
228 }
229
230 BoundedReader in = new BoundedReader( this.in, markLength );
231
232 int ch = in.read();
233 if ( ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
234 {
235 return ch;
236 }
237
238 boolean inEscape = useEscape && ch == getEscapeString().charAt( 0 );
239
240 StringBuilder key = new StringBuilder();
241
242 // have we found an escape string?
243 if ( inEscape )
244 {
245 for ( int i = 0; i < getEscapeString().length(); i++ )
246 {
247 key.append( (char) ch );
248
249 if ( ch != getEscapeString().charAt( i ) || ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
250 {
251 // mismatch, EOF or EOL, no escape string here
252 in.reset();
253 inEscape = false;
254 key.setLength( 0 );
255 break;
256 }
257
258 ch = in.read();
259
260 }
261
262 }
263
264 // have we found a delimiter?
265 int max = 0;
266 for ( DelimiterSpecification spec : delimiters )
267 {
268 String begin = spec.getBegin();
269
270 // longest match wins
271 if ( begin.length() < max )
272 {
273 continue;
274 }
275
276 for ( int i = 0; i < begin.length(); i++ )
277 {
278 if ( ch != begin.charAt( i ) || ch == -1 || ( ch == '\n' && !supportMultiLineFiltering ) )
279 {
280 // mismatch, EOF or EOL, no match
281 break;
282 }
283
284 if ( i == begin.length() - 1 )
285 {
286
287 beginToken = spec.getBegin();
288 endToken = spec.getEnd();
289
290 }
291
292 ch = in.read();
293
294 }
295
296 in.reset();
297 in.skip( key.length() );
298 ch = in.read();
299
300 }
301
302 // escape means no luck, prevent parsing of the escaped character, and return
303 if ( inEscape )
304 {
305
306 if ( beginToken != null )
307 {
308 if ( !isPreserveEscapeString() )
309 {
310 key.setLength( 0 );
311 }
312 }
313
314 beginToken = null;
315 endToken = null;
316
317 key.append( (char) ch );
318
319 replaceData = key.toString();
320 replaceIndex = key.length();
321
322 return read();
323
324 }
325
326 // no match means no luck, reset and return
327 if ( beginToken == null || beginToken.length() == 0 || endToken == null || endToken.length() == 0 )
328 {
329
330 in.reset();
331 return in.read();
332
333 }
334
335 // we're committed, find the end token, EOL or EOF
336
337 key.append( beginToken );
338 in.reset();
339 in.skip( beginToken.length() );
340 ch = in.read();
341
342 int endTokenSize = endToken.length();
343 int end = endTokenSize;
344 do
345 {
346 if ( ch == -1 )
347 {
348 break;
349 }
350 else if ( ch == '\n' && !supportMultiLineFiltering )
351 {
352 // EOL
353 key.append( (char) ch );
354 break;
355 }
356
357 key.append( (char) ch );
358
359 if ( ch == this.endToken.charAt( endTokenSize - end ) )
360 {
361 end--;
362 if ( end == 0 )
363 {
364 break;
365 }
366 }
367 else
368 {
369 end = endTokenSize;
370 }
371
372 ch = in.read();
373 }
374 while ( true );
375
376 // reset back to no tokens
377 beginToken = null;
378 endToken = null;
379
380 // found endtoken? interpolate our key resolved above
381 String value = null;
382 if ( end == 0 )
383 {
384 try
385 {
386 if ( interpolateWithPrefixPattern )
387 {
388 value = interpolator.interpolate( key.toString(), "", recursionInterceptor );
389 }
390 else
391 {
392 value = interpolator.interpolate( key.toString(), recursionInterceptor );
393 }
394 }
395 catch ( InterpolationException e )
396 {
397 IllegalArgumentException error = new IllegalArgumentException( e.getMessage() );
398 error.initCause( e );
399
400 throw error;
401 }
402 }
403 else
404 {
405 // no endtoken? Write current char and continue in search for next expression
406 in.reset();
407 return in.read();
408 }
409
410 // write away the value if present, otherwise the key unmodified
411 if ( value != null )
412 {
413 replaceData = value;
414 replaceIndex = value.length();
415 }
416 else
417 {
418 replaceData = key.toString();
419 replaceIndex = key.length();
420 }
421
422 if ( ch == -1 )
423 {
424 eof = true;
425 }
426 return read();
427
428 }
429
430 /**
431 * @return interpolate with prefix pattern {@code true} (active) {@code false} otherwise.
432 */
433 public boolean isInterpolateWithPrefixPattern()
434 {
435 return interpolateWithPrefixPattern;
436 }
437
438 /**
439 * @param interpolateWithPrefixPattern set the interpolate with prefix pattern.
440 */
441 public void setInterpolateWithPrefixPattern( boolean interpolateWithPrefixPattern )
442 {
443 this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
444 }
445
446 /**
447 * @return {@link RecursionInterceptor}
448 */
449 public RecursionInterceptor getRecursionInterceptor()
450 {
451 return recursionInterceptor;
452 }
453
454 /**
455 * @param givenRecursionInterceptor {@link RecursionInterceptor}
456 * @return this
457 */
458 // CHECKSTYLE_OFF: LineLength
459 public AbstractFilterReaderLineEnding setRecursionInterceptor( RecursionInterceptor givenRecursionInterceptor )
460 // CHECKSTYLE_ON: LineLength
461 {
462 this.recursionInterceptor = givenRecursionInterceptor;
463 return this;
464 }
465
466 }