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 }