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 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 }