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 }