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
25 import org.codehaus.plexus.interpolation.InterpolationException;
26 import org.codehaus.plexus.interpolation.Interpolator;
27 import org.codehaus.plexus.interpolation.RecursionInterceptor;
28 import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
29
30 /**
31 * A FilterReader implementation, that works with Interpolator interface instead of it's own interpolation
32 * implementation. This implementation is heavily based on org.codehaus.plexus.util.InterpolationFilterReader.
33 *
34 * @author cstamas
35 * @author Olivier Lamy
36 * @since 1.0
37 */
38 public class InterpolatorFilterReaderLineEnding extends AbstractFilterReaderLineEnding {
39
40 /**
41 * Interpolator used to interpolate
42 */
43 private Interpolator interpolator;
44
45 private RecursionInterceptor recursionInterceptor;
46
47 /**
48 * replacement text from a token
49 */
50 private String replaceData = null;
51
52 /**
53 * Index into replacement data
54 */
55 private int replaceIndex = 0;
56
57 /**
58 * Default begin token.
59 */
60 public static final String DEFAULT_BEGIN_TOKEN = "${";
61
62 /**
63 * Default end token.
64 */
65 public static final String DEFAULT_END_TOKEN = "}";
66
67 private String beginToken;
68
69 private String endToken;
70
71 /**
72 * true by default to preserve backward comp
73 */
74 private boolean interpolateWithPrefixPattern = true;
75
76 private boolean supportMultiLineFiltering;
77
78 private boolean eof = false;
79
80 /**
81 * @param in reader to use
82 * @param interpolator interpolator instance to use
83 * @param beginToken start token to use
84 * @param endToken end token to use
85 * @param supportMultiLineFiltering If multi line filtering is allowed
86 */
87 public InterpolatorFilterReaderLineEnding(
88 Reader in,
89 Interpolator interpolator,
90 String beginToken,
91 String endToken,
92 boolean supportMultiLineFiltering) {
93 this(in, interpolator, beginToken, endToken, new SimpleRecursionInterceptor(), supportMultiLineFiltering);
94 }
95
96 /**
97 * @param in reader to use
98 * @param interpolator interpolator instance to use
99 * @param beginToken start token to use
100 * @param endToken end token to use
101 * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
102 * @param supportMultiLineFiltering If multi line filtering is allowed
103 */
104 private InterpolatorFilterReaderLineEnding(
105 Reader in,
106 Interpolator interpolator,
107 String beginToken,
108 String endToken,
109 RecursionInterceptor ri,
110 boolean supportMultiLineFiltering) {
111 // wrap our own buffer, so we can use mark/reset safely.
112 super(new BufferedReader(in));
113
114 this.interpolator = interpolator;
115
116 this.beginToken = beginToken;
117
118 this.endToken = endToken;
119
120 recursionInterceptor = ri;
121
122 this.supportMultiLineFiltering = supportMultiLineFiltering;
123
124 calculateMarkLength();
125 }
126
127 /**
128 * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
129 * the stream is reached.
130 *
131 * @param n The number of characters to skip
132 * @return the number of characters actually skipped
133 * @throws IOException If an I/O error occurs
134 */
135 @Override
136 public long skip(long n) throws IOException {
137 if (n < 0L) {
138 throw new IllegalArgumentException("skip value is negative");
139 }
140
141 for (long i = 0; i < n; i++) {
142 if (read() == -1) {
143 return i;
144 }
145 }
146 return n;
147 }
148
149 /**
150 * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
151 * occurs, or the end of the stream is reached.
152 *
153 * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
154 * @param off Offset at which to start storing characters.
155 * @param len Maximum number of characters to read.
156 * @return the number of characters read, or -1 if the end of the stream has been reached
157 * @throws IOException If an I/O error occurs
158 */
159 @Override
160 public int read(char[] cbuf, int off, int len) throws IOException {
161 for (int i = 0; i < len; i++) {
162 int ch = read();
163 if (ch == -1) {
164 if (i == 0) {
165 return -1;
166 } else {
167 return i;
168 }
169 }
170 cbuf[off + i] = (char) ch;
171 }
172 return len;
173 }
174
175 /**
176 * Returns the next character in the filtered stream, replacing tokens from the original stream.
177 *
178 * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
179 * @throws IOException if the underlying stream throws an IOException during reading
180 */
181 @Override
182 public int read() throws IOException {
183 if (replaceIndex > 0) {
184 return replaceData.charAt(replaceData.length() - (replaceIndex--));
185 }
186 if (eof) {
187 return -1;
188 }
189
190 in.mark(markLength);
191
192 int ch = in.read();
193 if (ch == -1 || (ch == '\n' && !supportMultiLineFiltering)) {
194 return ch;
195 }
196
197 boolean inEscape = useEscape && ch == getEscapeString().charAt(0);
198
199 StringBuilder key = new StringBuilder();
200
201 // have we found an escape string?
202 if (inEscape) {
203 for (int i = 0; i < getEscapeString().length(); i++) {
204 key.append((char) ch);
205
206 if (ch != getEscapeString().charAt(i) || ch == -1 || (ch == '\n' && !supportMultiLineFiltering)) {
207 // mismatch, EOF or EOL, no escape string here
208 in.reset();
209 inEscape = false;
210 key.setLength(0);
211 break;
212 }
213
214 ch = in.read();
215 }
216 }
217
218 // have we found a delimiter?
219 boolean foundToken = false;
220 for (int i = 0; i < beginToken.length(); i++) {
221 if (ch != beginToken.charAt(i) || ch == -1 || (ch == '\n' && !supportMultiLineFiltering)) {
222 // mismatch, EOF or EOL, no match
223 break;
224 }
225
226 if (i == beginToken.length() - 1) {
227
228 foundToken = true;
229 }
230
231 ch = in.read();
232 }
233
234 in.reset();
235 in.skip(key.length());
236 ch = in.read();
237
238 // escape means no luck, prevent parsing of the escaped character, and return
239 if (inEscape) {
240
241 if (beginToken != null) {
242 if (!isPreserveEscapeString()) {
243 key.setLength(0);
244 }
245 }
246
247 key.append((char) ch);
248
249 replaceData = key.toString();
250 replaceIndex = key.length();
251
252 return read();
253 }
254
255 // no match means no luck, reset and return
256 if (!foundToken) {
257
258 in.reset();
259 return in.read();
260 }
261
262 // we're committed, find the end token, EOL or EOF
263
264 key.append(beginToken);
265 in.reset();
266 in.skip(beginToken.length());
267 ch = in.read();
268
269 int endTokenSize = endToken.length();
270 int end = endTokenSize;
271 do {
272 if (ch == -1) {
273 break;
274 } else if (ch == '\n' && !supportMultiLineFiltering) {
275 // EOL
276 key.append((char) ch);
277 break;
278 }
279
280 key.append((char) ch);
281
282 if (ch == this.endToken.charAt(endTokenSize - end)) {
283 end--;
284 if (end == 0) {
285 break;
286 }
287 } else {
288 end = endTokenSize;
289 }
290
291 ch = in.read();
292 } while (true);
293
294 // found endtoken? interpolate our key resolved above
295 String value = null;
296 if (end == 0) {
297 try {
298 if (interpolateWithPrefixPattern) {
299 value = interpolator.interpolate(key.toString(), "", recursionInterceptor);
300 } else {
301 value = interpolator.interpolate(key.toString(), recursionInterceptor);
302 }
303 } catch (InterpolationException e) {
304 throw new IllegalArgumentException(e.getMessage(), e);
305 }
306 }
307
308 // write away the value if present, otherwise the key unmodified
309 if (value != null) {
310 replaceData = value;
311 replaceIndex = value.length();
312 } else {
313 replaceData = key.toString();
314 replaceIndex = key.length();
315 }
316
317 if (ch == -1) {
318 eof = true;
319 }
320 return read();
321 }
322
323 /**
324 * @return current state of interpolate with prefix pattern.
325 */
326 public boolean isInterpolateWithPrefixPattern() {
327 return interpolateWithPrefixPattern;
328 }
329
330 /**
331 * @param interpolateWithPrefixPattern interpolate with prefix pattern.
332 */
333 public void setInterpolateWithPrefixPattern(boolean interpolateWithPrefixPattern) {
334 this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
335 }
336
337 /**
338 * @return {@link #recursionInterceptor}
339 */
340 public RecursionInterceptor getRecursionInterceptor() {
341 return recursionInterceptor;
342 }
343
344 /**
345 * @param theRecursionInterceptor {@link RecursionInterceptor}
346 * @return {@link InterpolatorFilterReaderLineEnding}
347 */
348 public InterpolatorFilterReaderLineEnding setRecursionInterceptor(RecursionInterceptor theRecursionInterceptor) {
349 this.recursionInterceptor = theRecursionInterceptor;
350 return this;
351 }
352 }