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.multi;
56  
57  import java.io.FilterReader;
58  import java.io.IOException;
59  import java.io.Reader;
60  import java.util.LinkedHashSet;
61  
62  import org.codehaus.plexus.interpolation.InterpolationException;
63  import org.codehaus.plexus.interpolation.Interpolator;
64  import org.codehaus.plexus.interpolation.RecursionInterceptor;
65  import org.codehaus.plexus.interpolation.SimpleRecursionInterceptor;
66  
67  /**
68   * A FilterReader implementation, that works with Interpolator interface instead of it's own interpolation
69   * implementation. This implementation is heavily based on org.codehaus.plexus.util.InterpolationFilterReader.
70   *
71   * @author cstamas
72   */
73  public class MultiDelimiterInterpolatorFilterReader extends FilterReader {
74  
75      /** Interpolator used to interpolate */
76      private Interpolator interpolator;
77  
78      /**
79       * @since 1.12
80       */
81      private RecursionInterceptor recursionInterceptor;
82  
83      /** replacement text from a token */
84      private String replaceData = null;
85  
86      /** Index into replacement data */
87      private int replaceIndex = -1;
88  
89      /** Index into previous data */
90      private int previousIndex = -1;
91  
92      /** Default begin token. */
93      public static final String DEFAULT_BEGIN_TOKEN = "${";
94  
95      /** Default end token. */
96      public static final String DEFAULT_END_TOKEN = "}";
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     private LinkedHashSet<DelimiterSpecification> delimiters = new LinkedHashSet<DelimiterSpecification>();
109 
110     private DelimiterSpecification currentSpec;
111 
112     private String beginToken;
113 
114     private String originalBeginToken;
115 
116     private String endToken;
117 
118     /**
119      * this constructor use default begin token ${ and default end token }
120      * @param in reader to use
121      * @param interpolator interpolator instance to use
122      */
123     public MultiDelimiterInterpolatorFilterReader(Reader in, Interpolator interpolator) {
124         this(in, interpolator, new SimpleRecursionInterceptor());
125     }
126 
127     /**
128      * @param in reader to use
129      * @param interpolator interpolator instance to use
130      * @param ri The {@link RecursionInterceptor} to use to prevent recursive expressions.
131      * @since 1.12
132      */
133     public MultiDelimiterInterpolatorFilterReader(Reader in, Interpolator interpolator, RecursionInterceptor ri) {
134         super(in);
135 
136         this.interpolator = interpolator;
137 
138         // always cache answers, since we'll be sending in pure expressions, not mixed text.
139         this.interpolator.setCacheAnswers(true);
140 
141         recursionInterceptor = ri;
142 
143         delimiters.add(DelimiterSpecification.DEFAULT_SPEC);
144     }
145 
146     public MultiDelimiterInterpolatorFilterReader addDelimiterSpec(String delimiterSpec) {
147         if (delimiterSpec == null) {
148             return this;
149         }
150         delimiters.add(DelimiterSpecification.parse(delimiterSpec));
151         return this;
152     }
153 
154     public boolean removeDelimiterSpec(String delimiterSpec) {
155         if (delimiterSpec == null) {
156             return false;
157         }
158         return delimiters.remove(DelimiterSpecification.parse(delimiterSpec));
159     }
160 
161     public MultiDelimiterInterpolatorFilterReader setDelimiterSpecs(LinkedHashSet<String> specs) {
162         delimiters.clear();
163         for (String spec : specs) {
164             if (spec == null) {
165                 continue;
166             }
167             delimiters.add(DelimiterSpecification.parse(spec));
168         }
169 
170         return this;
171     }
172 
173     /**
174      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
175      * the stream is reached.
176      *
177      * @param n The number of characters to skip
178      * @return the number of characters actually skipped
179      * @exception IllegalArgumentException If <code>n</code> is negative.
180      * @exception IOException If an I/O error occurs
181      */
182     public long skip(long n) throws IOException {
183         if (n < 0L) {
184             throw new IllegalArgumentException("skip value is negative");
185         }
186 
187         for (long i = 0; i < n; i++) {
188             if (read() == -1) {
189                 return i;
190             }
191         }
192         return n;
193     }
194 
195     /**
196      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
197      * occurs, or the end of the stream is reached.
198      *
199      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
200      * @param off Offset at which to start storing characters.
201      * @param len Maximum number of characters to read.
202      * @return the number of characters read, or -1 if the end of the stream has been reached
203      * @exception IOException If an I/O error occurs
204      */
205     public int read(char cbuf[], int off, int len) throws IOException {
206         for (int i = 0; i < len; i++) {
207             int ch = read();
208             if (ch == -1) {
209                 if (i == 0) {
210                     return -1;
211                 } else {
212                     return i;
213                 }
214             }
215             cbuf[off + i] = (char) ch;
216         }
217         return len;
218     }
219 
220     /**
221      * Returns the next character in the filtered stream, replacing tokens from the original stream.
222      *
223      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
224      * @exception IOException if the underlying stream throws an IOException during reading
225      */
226     public int read() throws IOException {
227         if (replaceIndex != -1 && replaceIndex < replaceData.length()) {
228             int ch = replaceData.charAt(replaceIndex++);
229             if (replaceIndex >= replaceData.length()) {
230                 replaceIndex = -1;
231             }
232             return ch;
233         }
234 
235         int ch = -1;
236         if (previousIndex != -1 && previousIndex < this.endToken.length()) {
237             ch = this.endToken.charAt(previousIndex++);
238         } else {
239             ch = in.read();
240         }
241 
242         boolean inEscape = false;
243 
244         if ((inEscape = (useEscape && ch == escapeString.charAt(0))) || reselectDelimiterSpec(ch)) {
245             StringBuilder key = new StringBuilder();
246 
247             key.append((char) ch);
248 
249             // this will happen when we're using an escape string, and ONLY then.
250             boolean atEnd = false;
251 
252             if (inEscape) {
253                 for (int i = 0; i < escapeString.length() - 1; i++) {
254                     ch = in.read();
255                     if (ch == -1) {
256                         atEnd = true;
257                         break;
258                     }
259 
260                     key.append((char) ch);
261                 }
262 
263                 if (!atEnd) {
264                     ch = in.read();
265                     if (!reselectDelimiterSpec(ch)) {
266                         replaceData = key.toString();
267                         replaceIndex = 1;
268                         return replaceData.charAt(0);
269                     } else {
270                         key.append((char) ch);
271                     }
272                 }
273             }
274 
275             int beginTokenMatchPos = 1;
276             do {
277                 if (atEnd) {
278                     // didn't finish reading the escape string.
279                     break;
280                 }
281 
282                 if (previousIndex != -1 && previousIndex < this.endToken.length()) {
283                     ch = this.endToken.charAt(previousIndex++);
284                 } else {
285                     ch = in.read();
286                 }
287                 if (ch != -1) {
288                     key.append((char) ch);
289                     if ((beginTokenMatchPos < this.originalBeginToken.length())
290                             && (ch != this.originalBeginToken.charAt(beginTokenMatchPos))) {
291                         ch = -1; // not really EOF but to trigger code below
292                         break;
293                     }
294                 } else {
295                     break;
296                 }
297 
298                 beginTokenMatchPos++;
299             } while (ch != this.endToken.charAt(0));
300 
301             // now test endToken
302             if (ch != -1 && this.endToken.length() > 1) {
303                 int endTokenMatchPos = 1;
304 
305                 do {
306                     if (previousIndex != -1 && previousIndex < this.endToken.length()) {
307                         ch = this.endToken.charAt(previousIndex++);
308                     } else {
309                         ch = in.read();
310                     }
311 
312                     if (ch != -1) {
313                         key.append((char) ch);
314 
315                         if (ch != this.endToken.charAt(endTokenMatchPos++)) {
316                             ch = -1; // not really EOF but to trigger code below
317                             break;
318                         }
319 
320                     } else {
321                         break;
322                     }
323                 } while (endTokenMatchPos < this.endToken.length());
324             }
325 
326             // There is nothing left to read so we have the situation where the begin/end token
327             // are in fact the same and as there is nothing left to read we have got ourselves
328             // end of a token boundary so let it pass through.
329             if (ch == -1) {
330                 replaceData = key.toString();
331                 replaceIndex = 1;
332                 return replaceData.charAt(0);
333             }
334 
335             String value = null;
336             try {
337                 boolean escapeFound = false;
338                 if (useEscape) {
339                     if (key.toString().startsWith(beginToken)) {
340                         String keyStr = key.toString();
341                         if (!preserveEscapeString) {
342                             value = keyStr.substring(escapeString.length(), keyStr.length());
343                         } else {
344                             value = keyStr;
345                         }
346                         escapeFound = true;
347                     }
348                 }
349                 if (!escapeFound) {
350                     if (interpolateWithPrefixPattern) {
351                         value = interpolator.interpolate(key.toString(), "", recursionInterceptor);
352                     } else {
353                         value = interpolator.interpolate(key.toString(), recursionInterceptor);
354                     }
355                 }
356             } catch (InterpolationException e) {
357                 IllegalArgumentException error = new IllegalArgumentException(e.getMessage());
358                 error.initCause(e);
359 
360                 throw error;
361             }
362 
363             if (value != null) {
364                 if (value.length() != 0) {
365                     replaceData = value;
366                     replaceIndex = 0;
367                 }
368                 return read();
369             } else {
370                 previousIndex = 0;
371                 replaceData = key.substring(0, key.length() - this.endToken.length());
372                 replaceIndex = 0;
373                 return this.beginToken.charAt(0);
374             }
375         }
376 
377         return ch;
378     }
379 
380     private boolean reselectDelimiterSpec(int ch) {
381         for (DelimiterSpecification spec : delimiters) {
382             if (ch == spec.getBegin().charAt(0)) {
383                 currentSpec = spec;
384                 originalBeginToken = currentSpec.getBegin();
385                 beginToken = useEscape ? escapeString + originalBeginToken : originalBeginToken;
386                 endToken = currentSpec.getEnd();
387 
388                 return true;
389             }
390         }
391 
392         return false;
393     }
394 
395     public boolean isInterpolateWithPrefixPattern() {
396         return interpolateWithPrefixPattern;
397     }
398 
399     public void setInterpolateWithPrefixPattern(boolean interpolateWithPrefixPattern) {
400         this.interpolateWithPrefixPattern = interpolateWithPrefixPattern;
401     }
402 
403     public String getEscapeString() {
404         return escapeString;
405     }
406 
407     public void setEscapeString(String escapeString) {
408         // TODO NPE if escapeString is null ?
409         if (escapeString != null && escapeString.length() >= 1) {
410             this.escapeString = escapeString;
411             this.useEscape = escapeString != null && escapeString.length() >= 1;
412         }
413     }
414 
415     public boolean isPreserveEscapeString() {
416         return preserveEscapeString;
417     }
418 
419     public void setPreserveEscapeString(boolean preserveEscapeString) {
420         this.preserveEscapeString = preserveEscapeString;
421     }
422 
423     public RecursionInterceptor getRecursionInterceptor() {
424         return recursionInterceptor;
425     }
426 
427     public MultiDelimiterInterpolatorFilterReader setRecursionInterceptor(RecursionInterceptor recursionInterceptor) {
428         this.recursionInterceptor = recursionInterceptor;
429         return this;
430     }
431 }