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