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 extends FilterReader {
82 /** replacement text from a token */
83 private String replaceData = null;
84
85 /** Index into replacement data */
86 private int replaceIndex = -1;
87
88 /** Index into previous data */
89 private int previousIndex = -1;
90
91 /** Hashtable to hold the replacee-replacer pairs (String to String). */
92 private Map<?, Object> variables = new HashMap<Object, Object>();
93
94 /** Character marking the beginning of a token. */
95 private String beginToken;
96
97 /** Character marking the end of a token. */
98 private String endToken;
99
100 /** Length of begin token. */
101 private int beginTokenLength;
102
103 /** Length of end token. */
104 private int endTokenLength;
105
106 /** Default begin token. */
107 private static final String DEFAULT_BEGIN_TOKEN = "${";
108
109 /** Default end token. */
110 private static final String DEFAULT_END_TOKEN = "}";
111
112 /**
113 * Construct a Reader to interpolate values enclosed between the given delimiter tokens.
114 *
115 * @param in a Reader to be wrapped for interpolation.
116 * @param variables name/value pairs to be interpolated into the character stream.
117 * @param beginToken an interpolation target begins with this.
118 * @param endToken an interpolation target ends with this.
119 */
120 public InterpolationFilterReader(Reader in, Map<?, Object> variables, String beginToken, String endToken) {
121 super(in);
122
123 this.variables = variables;
124 this.beginToken = beginToken;
125 this.endToken = endToken;
126
127 beginTokenLength = beginToken.length();
128 endTokenLength = endToken.length();
129 }
130
131 /**
132 * Construct a Reader using the default interpolation delimiter tokens "${" and "}".
133 *
134 * @param in a Reader to be wrapped for interpolation.
135 * @param variables name/value pairs to be interpolated into the character stream.
136 */
137 public InterpolationFilterReader(Reader in, Map<String, Object> variables) {
138 this(in, variables, DEFAULT_BEGIN_TOKEN, DEFAULT_END_TOKEN);
139 }
140
141 /**
142 * Skips characters. This method will block until some characters are available, an I/O error occurs, or the end of
143 * the stream is reached.
144 *
145 * @param n The number of characters to skip
146 * @return the number of characters actually skipped
147 * @exception IllegalArgumentException If <code>n</code> is negative.
148 * @exception IOException If an I/O error occurs
149 */
150 @Override
151 public long skip(long n) throws IOException {
152 if (n < 0L) {
153 throw new IllegalArgumentException("skip value is negative");
154 }
155
156 for (long i = 0; i < n; i++) {
157 if (read() == -1) {
158 return i;
159 }
160 }
161 return n;
162 }
163
164 /**
165 * Reads characters into a portion of an array. This method will block until some input is available, an I/O error
166 * occurs, or the end of the stream is reached.
167 *
168 * @param cbuf Destination buffer to write characters to. Must not be <code>null</code>.
169 * @param off Offset at which to start storing characters.
170 * @param len Maximum number of characters to read.
171 * @return the number of characters read, or -1 if the end of the stream has been reached
172 * @exception IOException If an I/O error occurs
173 */
174 @Override
175 public int read(char cbuf[], int off, int len) throws IOException {
176 for (int i = 0; i < len; i++) {
177 int ch = read();
178 if (ch == -1) {
179 if (i == 0) {
180 return -1;
181 } else {
182 return i;
183 }
184 }
185 cbuf[off + i] = (char) ch;
186 }
187 return len;
188 }
189
190 /**
191 * Returns the next character in the filtered stream, replacing tokens from the original stream.
192 *
193 * @return the next character in the resulting stream, or -1 if the end of the resulting stream has been reached
194 * @exception IOException if the underlying stream throws an IOException during reading
195 */
196 @Override
197 public int read() throws IOException {
198 if (replaceIndex != -1 && replaceIndex < replaceData.length()) {
199 int ch = replaceData.charAt(replaceIndex++);
200 if (replaceIndex >= replaceData.length()) {
201 replaceIndex = -1;
202 }
203 return ch;
204 }
205
206 int ch;
207 if (previousIndex != -1 && previousIndex < endTokenLength) {
208 ch = endToken.charAt(previousIndex++);
209 } else {
210 ch = in.read();
211 }
212
213 if (ch == beginToken.charAt(0)) {
214 StringBuilder key = new StringBuilder();
215
216 int beginTokenMatchPos = 1;
217
218 do {
219 if (previousIndex != -1 && previousIndex < endTokenLength) {
220 ch = endToken.charAt(previousIndex++);
221 } else {
222 ch = in.read();
223 }
224 if (ch != -1) {
225 key.append((char) ch);
226
227 if ((beginTokenMatchPos < beginTokenLength) && (ch != beginToken.charAt(beginTokenMatchPos++))) {
228 ch = -1; // not really EOF but to trigger code below
229 break;
230 }
231 } else {
232 break;
233 }
234 } while (ch != endToken.charAt(0));
235
236 // now test endToken
237 if (ch != -1 && endTokenLength > 1) {
238 int endTokenMatchPos = 1;
239
240 do {
241 if (previousIndex != -1 && previousIndex < endTokenLength) {
242 ch = endToken.charAt(previousIndex++);
243 } else {
244 ch = in.read();
245 }
246
247 if (ch != -1) {
248 key.append((char) ch);
249
250 if (ch != endToken.charAt(endTokenMatchPos++)) {
251 ch = -1; // not really EOF but to trigger code below
252 break;
253 }
254
255 } else {
256 break;
257 }
258 } while (endTokenMatchPos < endTokenLength);
259 }
260
261 // There is nothing left to read so we have the situation where the begin/end token
262 // are in fact the same and as there is nothing left to read we have got ourselves
263 // end of a token boundary so let it pass through.
264 if (ch == -1) {
265 replaceData = key.toString();
266 replaceIndex = 0;
267 return beginToken.charAt(0);
268 }
269
270 String variableKey = key.substring(beginTokenLength - 1, key.length() - endTokenLength);
271
272 Object o = variables.get(variableKey);
273 if (o != null) {
274 String value = o.toString();
275 if (value.length() != 0) {
276 replaceData = value;
277 replaceIndex = 0;
278 }
279 return read();
280 } else {
281 previousIndex = 0;
282 replaceData = key.substring(0, key.length() - endTokenLength);
283 replaceIndex = 0;
284 return beginToken.charAt(0);
285 }
286 }
287
288 return ch;
289 }
290 }