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     @Override
155     public long skip( long n )
156         throws IOException
157     {
158         if ( n < 0L )
159         {
160             throw new IllegalArgumentException( "skip value is negative" );
161         }
162 
163         for ( long i = 0; i < n; i++ )
164         {
165             if ( read() == -1 )
166             {
167                 return i;
168             }
169         }
170         return n;
171     }
172 
173     /**
174      * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
175      * occurs, or the end of the stream is reached.
176      *
177      * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
178      * @param off Offset at which to start storing characters.
179      * @param len Maximum number of characters to read.
180      * @return the number of characters read, or -1 if the end of the stream has been reached
181      * @exception IOException If an I/O error occurs
182      */
183     @Override
184     public int read( char cbuf[], int off, int len )
185         throws IOException
186     {
187         for ( int i = 0; i < len; i++ )
188         {
189             int ch = read();
190             if ( ch == -1 )
191             {
192                 if ( i == 0 )
193                 {
194                     return -1;
195                 }
196                 else
197                 {
198                     return i;
199                 }
200             }
201             cbuf[off + i] = (char) ch;
202         }
203         return len;
204     }
205 
206     /**
207      * Returns the next character in the filtered stream, replacing tokens from the original stream.
208      *
209      * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
210      * @exception IOException if the underlying stream throws an IOException during reading
211      */
212     @Override
213     public int read()
214         throws IOException
215     {
216         if ( replaceIndex != -1 && replaceIndex < replaceData.length() )
217         {
218             int ch = replaceData.charAt( replaceIndex++ );
219             if ( replaceIndex >= replaceData.length() )
220             {
221                 replaceIndex = -1;
222             }
223             return ch;
224         }
225 
226         int ch;
227         if ( previousIndex != -1 && previousIndex < endTokenLength )
228         {
229             ch = endToken.charAt( previousIndex++ );
230         }
231         else
232         {
233             ch = in.read();
234         }
235 
236         if ( ch == beginToken.charAt( 0 ) )
237         {
238             StringBuilder key = new StringBuilder();
239 
240             int beginTokenMatchPos = 1;
241 
242             do
243             {
244                 if ( previousIndex != -1 && previousIndex < endTokenLength )
245                 {
246                     ch = endToken.charAt( previousIndex++ );
247                 }
248                 else
249                 {
250                     ch = in.read();
251                 }
252                 if ( ch != -1 )
253                 {
254                     key.append( (char) ch );
255 
256                     if ( ( beginTokenMatchPos < beginTokenLength )
257                         && ( ch != beginToken.charAt( beginTokenMatchPos++ ) ) )
258                     {
259                         ch = -1; // not really EOF but to trigger code below
260                         break;
261                     }
262                 }
263                 else
264                 {
265                     break;
266                 }
267             }
268             while ( ch != endToken.charAt( 0 ) );
269 
270             // now test endToken
271             if ( ch != -1 && endTokenLength > 1 )
272             {
273                 int endTokenMatchPos = 1;
274 
275                 do
276                 {
277                     if ( previousIndex != -1 && previousIndex < endTokenLength )
278                     {
279                         ch = endToken.charAt( previousIndex++ );
280                     }
281                     else
282                     {
283                         ch = in.read();
284                     }
285 
286                     if ( ch != -1 )
287                     {
288                         key.append( (char) ch );
289 
290                         if ( ch != endToken.charAt( endTokenMatchPos++ ) )
291                         {
292                             ch = -1; // not really EOF but to trigger code below
293                             break;
294                         }
295 
296                     }
297                     else
298                     {
299                         break;
300                     }
301                 }
302                 while ( endTokenMatchPos < endTokenLength );
303             }
304 
305             // There is nothing left to read so we have the situation where the begin/end token
306             // are in fact the same and as there is nothing left to read we have got ourselves
307             // end of a token boundary so let it pass through.
308             if ( ch == -1 )
309             {
310                 replaceData = key.toString();
311                 replaceIndex = 0;
312                 return beginToken.charAt( 0 );
313             }
314 
315             String variableKey = key.substring( beginTokenLength - 1, key.length() - endTokenLength );
316 
317             Object o = variables.get( variableKey );
318             if ( o != null )
319             {
320                 String value = o.toString();
321                 if ( value.length() != 0 )
322                 {
323                     replaceData = value;
324                     replaceIndex = 0;
325                 }
326                 return read();
327             }
328             else
329             {
330                 previousIndex = 0;
331                 replaceData = key.substring( 0, key.length() - endTokenLength );
332                 replaceIndex = 0;
333                 return beginToken.charAt( 0 );
334             }
335         }
336 
337         return ch;
338     }
339 }