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 acknowledgement:
21   *       "This product includes software developed by the
22   *        Apache Software Foundation (http://www.codehaus.org/)."
23   *    Alternately, this acknowledgement may appear in the software itself,
24   *    if and wherever such third-party acknowledgements 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.util;
56  
57  import java.io.FilterReader;
58  import java.io.IOException;
59  import java.io.Reader;
60  import java.util.HashMap;
61  import java.util.Map;
62  
63  /**
64   * A FilterReader which interpolates keyword values into a character stream. Keywords are recognized when enclosed
65   * between starting and ending delimiter strings. The keywords themselves, and their values, are fetched from a Map
66   * supplied to the constructor.
67   * <p>
68   * When a possible keyword token is recognized (by detecting the starting and ending token delimiters):
69   * </p>
70   * <ul>
71   * <li>if the enclosed string is found in the keyword Map, the delimiters and the keyword are effectively replaced by
72   * the keyword's value;</li>
73   * <li>if the enclosed string is found in the keyword Map, but its value has zero length, then the token (delimiters and
74   * keyword) is effectively removed from the character stream;</li>
75   * <li>if the enclosed string is <em>not</em> found in the keyword Map, then no substitution is made; the token text is
76   * passed through unaltered.</li>
77   * </ul>
78   * 
79   * @see LineOrientedInterpolatingReader s
80   */
81  public class InterpolationFilterReader
82      extends FilterReader
83  {
84      /** replacement text from a token */
85      private String replaceData = null;
86  
87      /** Index into replacement data */
88      private int replaceIndex = -1;
89  
90      /** Index into previous data */
91      private int previousIndex = -1;
92  
93      /** Hashtable to hold the replacee-replacer pairs (String to String). */
94      private Map<?, Object> variables = new HashMap<Object, Object>();
95  
96      /** Character marking the beginning of a token. */
97      private String beginToken;
98  
99      /** Character marking the end of a token. */
100     private String endToken;
101 
102     /** Length of begin token. */
103     private int beginTokenLength;
104 
105     /** Length of end token. */
106     private int endTokenLength;
107 
108     /** Default begin token. */
109     private static final String DEFAULT_BEGIN_TOKEN = "${";
110 
111     /** Default end token. */
112     private static final String DEFAULT_END_TOKEN = "}";
113 
114     /**
115      * Construct a Reader to interpolate values enclosed between the given delimiter tokens.
116      * 
117      * @param in a Reader to be wrapped for interpolation.
118      * @param variables name/value pairs to be interpolated into the character stream.
119      * @param beginToken an interpolation target begins with this.
120      * @param endToken an interpolation target ends with this.
121      */
122     public InterpolationFilterReader( Reader in, Map<?, Object> variables, String beginToken, String endToken )
123     {
124         super( in );
125 
126         this.variables = variables;
127         this.beginToken = beginToken;
128         this.endToken = endToken;
129 
130         beginTokenLength = beginToken.length();
131         endTokenLength = endToken.length();
132     }
133 
134     /**
135      * Construct a Reader using the default interpolation delimiter tokens "${" and "}".
136      * 
137      * @param in a Reader to be wrapped for interpolation.
138      * @param variables name/value pairs to be interpolated into the character stream.
139      */
140     public InterpolationFilterReader( Reader in, Map<String, Object> variables )
141     {
142         this( in, variables, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN );
143     }
144 
145     /**
146      * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
147      * the stream is reached.
148      *
149      * @param n The number of characters to skip
150      * @return the number of characters actually skipped
151      * @exception IllegalArgumentException If <code>n</code> is negative.
152      * @exception IOException If an I/O error occurs
153      */
154     public long skip( long n )
155         throws IOException
156     {
157         if ( n < 0L )
158         {
159             throw new IllegalArgumentException( "skip value is negative" );
160         }
161 
162         for ( long i = 0; i < n; i++ )
163         {
164             if ( read() == -1 )
165             {
166                 return i;
167             }
168         }
169         return n;
170     }
171 
172     /**
173      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
174      * occurs, or the end of the stream is reached.
175      *
176      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
177      * @param off Offset at which to start storing characters.
178      * @param len Maximum number of characters to read.
179      * @return the number of characters read, or -1 if the end of the stream has been reached
180      * @exception IOException If an I/O error occurs
181      */
182     public int read( char cbuf[], int off, int len )
183         throws IOException
184     {
185         for ( int i = 0; i < len; i++ )
186         {
187             int ch = read();
188             if ( ch == -1 )
189             {
190                 if ( i == 0 )
191                 {
192                     return -1;
193                 }
194                 else
195                 {
196                     return i;
197                 }
198             }
199             cbuf[off + i] = (char) ch;
200         }
201         return len;
202     }
203 
204     /**
205      * Returns the next character in the filtered stream, replacing tokens from the original stream.
206      *
207      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
208      * @exception IOException if the underlying stream throws an IOException during reading
209      */
210     public int read()
211         throws IOException
212     {
213         if ( replaceIndex != -1 && replaceIndex < replaceData.length() )
214         {
215             int ch = replaceData.charAt( replaceIndex++ );
216             if ( replaceIndex >= replaceData.length() )
217             {
218                 replaceIndex = -1;
219             }
220             return ch;
221         }
222 
223         int ch;
224         if ( previousIndex != -1 && previousIndex < endTokenLength )
225         {
226             ch = endToken.charAt( previousIndex++ );
227         }
228         else
229         {
230             ch = in.read();
231         }
232 
233         if ( ch == beginToken.charAt( 0 ) )
234         {
235             StringBuilder key = new StringBuilder();
236 
237             int beginTokenMatchPos = 1;
238 
239             do
240             {
241                 if ( previousIndex != -1 && previousIndex < endTokenLength )
242                 {
243                     ch = endToken.charAt( previousIndex++ );
244                 }
245                 else
246                 {
247                     ch = in.read();
248                 }
249                 if ( ch != -1 )
250                 {
251                     key.append( (char) ch );
252 
253                     if ( ( beginTokenMatchPos < beginTokenLength )
254                         && ( ch != beginToken.charAt( beginTokenMatchPos++ ) ) )
255                     {
256                         ch = -1; // not really EOF but to trigger code below
257                         break;
258                     }
259                 }
260                 else
261                 {
262                     break;
263                 }
264             }
265             while ( ch != endToken.charAt( 0 ) );
266 
267             // now test endToken
268             if ( ch != -1 && endTokenLength > 1 )
269             {
270                 int endTokenMatchPos = 1;
271 
272                 do
273                 {
274                     if ( previousIndex != -1 && previousIndex < endTokenLength )
275                     {
276                         ch = endToken.charAt( previousIndex++ );
277                     }
278                     else
279                     {
280                         ch = in.read();
281                     }
282 
283                     if ( ch != -1 )
284                     {
285                         key.append( (char) ch );
286 
287                         if ( ch != endToken.charAt( endTokenMatchPos++ ) )
288                         {
289                             ch = -1; // not really EOF but to trigger code below
290                             break;
291                         }
292 
293                     }
294                     else
295                     {
296                         break;
297                     }
298                 }
299                 while ( endTokenMatchPos < endTokenLength );
300             }
301 
302             // There is nothing left to read so we have the situation where the begin/end token
303             // are in fact the same and as there is nothing left to read we have got ourselves
304             // end of a token boundary so let it pass through.
305             if ( ch == -1 )
306             {
307                 replaceData = key.toString();
308                 replaceIndex = 0;
309                 return beginToken.charAt( 0 );
310             }
311 
312             String variableKey = key.substring( beginTokenLength - 1, key.length() - endTokenLength );
313 
314             Object o = variables.get( variableKey );
315             if ( o != null )
316             {
317                 String value = o.toString();
318                 if ( value.length() != 0 )
319                 {
320                     replaceData = value;
321                     replaceIndex = 0;
322                 }
323                 return read();
324             }
325             else
326             {
327                 previousIndex = 0;
328                 replaceData = key.substring( 0, key.length() - endTokenLength );
329                 replaceIndex = 0;
330                 return beginToken.charAt( 0 );
331             }
332         }
333 
334         return ch;
335     }
336 }