View Javadoc
1   /*
2    * The Apache Software License, Version 1.1
3    *
4    * Copyright (c) 2002-2003 The Apache Software Foundation.  All rights
5    * reserved.
6    *
7    * Redistribution and use in source and binary forms, with or without
8    * modification, are permitted provided that the following conditions
9    * are met:
10   *
11   * 1. Redistributions of source code must retain the above copyright
12   *    notice, this list of conditions and the following disclaimer.
13   *
14   * 2. Redistributions in binary form must reproduce the above copyright
15   *    notice, this list of conditions and the following disclaimer in
16   *    the documentation and/or other materials provided with the
17   *    distribution.
18   *
19   * 3. The end-user documentation included with the redistribution, if
20   *    any, must include the following acknowlegement:
21   *       "This product includes software developed by the
22   *        Apache Software Foundation (http://www.codehaus.org/)."
23   *    Alternately, this acknowlegement may appear in the software itself,
24   *    if and wherever such third-party acknowlegements normally appear.
25   *
26   * 4. The names "Ant" and "Apache Software
27   *    Foundation" must not be used to endorse or promote products derived
28   *    from this software without prior written permission. For written
29   *    permission, please contact codehaus@codehaus.org.
30   *
31   * 5. Products derived from this software may not be called "Apache"
32   *    nor may "Apache" appear in their names without prior written
33   *    permission of the Apache Group.
34   *
35   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
36   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
37   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
39   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
42   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
44   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
45   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
46   * SUCH DAMAGE.
47   * ====================================================================
48   *
49   * This software consists of voluntary contributions made by many
50   * individuals on behalf of the Apache Software Foundation.  For more
51   * information on the Apache Software Foundation, please see
52   * <http://www.codehaus.org/>.
53   */
54  
55  package org.codehaus.plexus.interpolation;
56  
57  import java.io.FilterReader;
58  import java.io.IOException;
59  import java.io.Reader;
60  
61  /**
62   * A FilterReader implementation, that works with Interpolator interface instead of it's own interpolation
63   * implementation. This implementation is heavily based on org.codehaus.plexus.util.InterpolationFilterReader.
64   *
65   * @author cstamas
66   */
67  public class InterpolatorFilterReader extends FilterReader {
68  
69      /** Interpolator used to interpolate */
70      private Interpolator interpolator;
71  
72      /**
73       * @since 1.12
74       */
75      private RecursionInterceptor recursionInterceptor;
76  
77      /** replacement text from a token */
78      private String replaceData = null;
79  
80      /** Index into replacement data */
81      private int replaceIndex = -1;
82  
83      /** Index into previous data */
84      private int previousIndex = -1;
85  
86      /** Default begin token. */
87      public static final String DEFAULT_BEGIN_TOKEN = "${";
88  
89      /** Default end token. */
90      public static final String DEFAULT_END_TOKEN = "}";
91  
92      private String beginToken;
93  
94      private String orginalBeginToken;
95  
96      private String endToken;
97  
98      /** true by default to preserve backward comp */
99      private boolean interpolateWithPrefixPattern = true;
100 
101     private String escapeString;
102 
103     private boolean useEscape = false;
104 
105     /** if true escapeString will be preserved \{foo} -> \{foo} */
106     private boolean preserveEscapeString = false;
107 
108     /**
109      * this constructor use default begin token ${ and default end token }
110      * @param in reader to use
111      * @param interpolator interpolator instance to use
112      */
113     public InterpolatorFilterReader(Reader in, Interpolator interpolator) {
114         this(in, interpolator, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN);
115     }
116 
117     /**
118      * @param in reader to use
119      * @param interpolator interpolator instance to use
120      * @param beginToken start token to use
121      * @param endToken end token to use
122      */
123     public InterpolatorFilterReader(Reader in, Interpolator interpolator, String beginToken, String endToken) {
124         this(in, interpolator, beginToken, endToken, new SimpleRecursionInterceptor());
125     }
126 
127     /**
128      * this constructor use default begin token ${ and default end token }
129      * @param in reader to use
130      * @param interpolator interpolator instance to use
131      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
132      * @since 1.12
133      */
134     public InterpolatorFilterReader(Reader in, Interpolator interpolator, RecursionInterceptor ri) {
135         this(in, interpolator, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN, ri);
136     }
137 
138     /**
139      * @param in reader to use
140      * @param interpolator interpolator instance to use
141      * @param beginToken start token to use
142      * @param endToken end token to use
143      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
144      * @since 1.12
145      */
146     public InterpolatorFilterReader(
147             Reader in, Interpolator interpolator, String beginToken, String endToken, RecursionInterceptor ri) {
148         super(in);
149 
150         this.interpolator = interpolator;
151 
152         this.beginToken = beginToken;
153 
154         this.endToken = endToken;
155 
156         recursionInterceptor = ri;
157 
158         this.orginalBeginToken = this.beginToken;
159     }
160 
161     /**
162      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
163      * the stream is reached.
164      *
165      * @param n The number of characters to skip
166      * @return the number of characters actually skipped
167      * @exception IllegalArgumentException If <code>n</code> is negative.
168      * @exception IOException If an I/O error occurs
169      */
170     public long skip(long n) throws IOException {
171         if (n < 0L) {
172             throw new IllegalArgumentException("skip value is negative");
173         }
174 
175         for (long i = 0; i < n; i++) {
176             if (read() == -1) {
177                 return i;
178             }
179         }
180         return n;
181     }
182 
183     /**
184      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
185      * occurs, or the end of the stream is reached.
186      *
187      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
188      * @param off Offset at which to start storing characters.
189      * @param len Maximum number of characters to read.
190      * @return the number of characters read, or -1 if the end of the stream has been reached
191      * @exception IOException If an I/O error occurs
192      */
193     public int read(char cbuf[], int off, int len) throws IOException {
194         for (int i = 0; i < len; i++) {
195             int ch = read();
196             if (ch == -1) {
197                 if (i == 0) {
198                     return -1;
199                 } else {
200                     return i;
201                 }
202             }
203             cbuf[off + i] = (char) ch;
204         }
205         return len;
206     }
207 
208     /**
209      * Returns the next character in the filtered stream, replacing tokens from the original stream.
210      *
211      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
212      * @exception IOException if the underlying stream throws an IOException during reading
213      */
214     public int read() throws IOException {
215         if (replaceIndex != -1 && replaceIndex < replaceData.length()) {
216             int ch = replaceData.charAt(replaceIndex++);
217             if (replaceIndex >= replaceData.length()) {
218                 replaceIndex = -1;
219             }
220             return ch;
221         }
222 
223         int ch = -1;
224         if (previousIndex != -1 && previousIndex < this.endToken.length()) {
225             ch = this.endToken.charAt(previousIndex++);
226         } else {
227             ch = in.read();
228         }
229 
230         if (ch == this.beginToken.charAt(0) || (useEscape && ch == this.orginalBeginToken.charAt(0))) {
231             StringBuilder key = new StringBuilder();
232 
233             key.append((char) ch);
234 
235             int beginTokenMatchPos = 1;
236 
237             do {
238                 if (previousIndex != -1 && previousIndex < this.endToken.length()) {
239                     ch = this.endToken.charAt(previousIndex++);
240                 } else {
241                     ch = in.read();
242                 }
243                 if (ch != -1) {
244                     key.append((char) ch);
245                     if ((beginTokenMatchPos < this.beginToken.length())
246                             && (ch != this.beginToken.charAt(beginTokenMatchPos++))
247                             && (useEscape
248                                     && this.orginalBeginToken.length() > (beginTokenMatchPos - 1)
249                                     && ch != this.orginalBeginToken.charAt(beginTokenMatchPos - 1))) {
250                         ch = -1; // not really EOF but to trigger code below
251                         break;
252                     }
253                 } else {
254                     break;
255                 }
256                 // MSHARED-81 olamy : we must take care of token with length 1, escaping and same char : \@foo@
257                 // here ch == endToken == beginToken -> not going to next char : bad :-)
258                 if (useEscape
259                         && this.orginalBeginToken == this.endToken
260                         && key.toString().startsWith(this.beginToken)) {
261                     ch = in.read();
262                     key.append((char) ch);
263                 }
264             } while (ch != this.endToken.charAt(0));
265 
266             // now test endToken
267             if (ch != -1 && this.endToken.length() > 1) {
268                 int endTokenMatchPos = 1;
269 
270                 do {
271                     if (previousIndex != -1 && previousIndex < this.endToken.length()) {
272                         ch = this.endToken.charAt(previousIndex++);
273                     } else {
274                         ch = in.read();
275                     }
276 
277                     if (ch != -1) {
278                         key.append((char) ch);
279 
280                         if (ch != this.endToken.charAt(endTokenMatchPos++)) {
281                             ch = -1; // not really EOF but to trigger code below
282                             break;
283                         }
284 
285                     } else {
286                         break;
287                     }
288                 } while (endTokenMatchPos < this.endToken.length());
289             }
290 
291             // There is nothing left to read so we have the situation where the begin/end token
292             // are in fact the same and as there is nothing left to read we have got ourselves
293             // end of a token boundary so let it pass through.
294             if (ch == -1) {
295                 replaceData = key.toString();
296                 replaceIndex = 1;
297                 return replaceData.charAt(0);
298             }
299 
300             String value = null;
301             try {
302                 boolean escapeFound = false;
303                 if (useEscape) {
304                     if (key.toString().startsWith(escapeString + orginalBeginToken)) {
305                         String keyStr = key.toString();
306                         if (!preserveEscapeString) {
307                             value = keyStr.substring(escapeString.length(), keyStr.length());
308                         } else {
309                             value = keyStr;
310                         }
311                         escapeFound = true;
312                     }
313                 }
314                 if (!escapeFound) {
315                     if (interpolateWithPrefixPattern) {
316                         value = interpolator.interpolate(key.toString(), "", recursionInterceptor);
317                     } else {
318                         value = interpolator.interpolate(key.toString(), recursionInterceptor);
319                     }
320                 }
321             } catch (InterpolationException e) {
322                 IllegalArgumentException error = new IllegalArgumentException(e.getMessage());
323                 error.initCause(e);
324 
325                 throw error;
326             }
327 
328             if (value != null) {
329                 if (value.length() != 0) {
330                     replaceData = value;
331                     replaceIndex = 0;
332                 }
333                 return read();
334             } else {
335                 previousIndex = 0;
336                 replaceData = key.substring(0, key.length() - this.endToken.length());
337                 replaceIndex = 0;
338                 return this.beginToken.charAt(0);
339             }
340         }
341 
342         return ch;
343     }
344 
345     public boolean isInterpolateWithPrefixPattern() {
346         return interpolateWithPrefixPattern;
347     }
348 
349     public void setInterpolateWithPrefixPattern(boolean interpolateWithPrefixPattern) {
350         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
351     }
352 
353     public String getEscapeString() {
354         return escapeString;
355     }
356 
357     public void setEscapeString(String escapeString) {
358         // TODO NPE if escapeString is null ?
359         if (escapeString != null && escapeString.length() >= 1) {
360             this.escapeString = escapeString;
361             this.orginalBeginToken = beginToken;
362             this.beginToken = escapeString + beginToken;
363             this.useEscape = escapeString != null && escapeString.length() >= 1;
364         }
365     }
366 
367     public boolean isPreserveEscapeString() {
368         return preserveEscapeString;
369     }
370 
371     public void setPreserveEscapeString(boolean preserveEscapeString) {
372         this.preserveEscapeString = preserveEscapeString;
373     }
374 
375     public RecursionInterceptor getRecursionInterceptor() {
376         return recursionInterceptor;
377     }
378 
379     public InterpolatorFilterReader setRecursionInterceptor(RecursionInterceptor recursionInterceptor) {
380         this.recursionInterceptor = recursionInterceptor;
381         return this;
382     }
383 }