001/* 002 * IzPack - Copyright 2001-2005 Julien Ponge, All Rights Reserved. 003 * 004 * http://www.izforge.com/izpack/ 005 * http://developer.berlios.de/projects/izpack/ 006 * 007 * Copyright 2001 Johannes Lehtinen 008 * 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 */ 021 022package com.izforge.izpack.util; 023 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.InputStreamReader; 027import java.io.OutputStream; 028import java.io.OutputStreamWriter; 029import java.io.Reader; 030import java.io.Serializable; 031import java.io.StringReader; 032import java.io.StringWriter; 033import java.io.UnsupportedEncodingException; 034import java.io.Writer; 035import java.util.HashMap; 036import java.util.Map; 037import java.util.Properties; 038 039/** 040 * Substitutes variables occurring in an input stream or a string. This implementation supports a 041 * generic variable value mapping and escapes the possible special characters occurring in the 042 * substituted values. The file types specifically supported are plain text files (no escaping), 043 * Java properties files, and XML files. A valid variable name matches the regular expression 044 * [a-zA-Z][a-zA-Z0-9_]* and names are case sensitive. Variables are referenced either by $NAME or 045 * ${NAME} (the latter syntax being useful in situations like ${NAME}NOTPARTOFNAME). If a referenced 046 * variable is undefined then it is not substituted but the corresponding part of the stream is 047 * copied as is. 048 * 049 * @author Johannes Lehtinen <johannes.lehtinen@iki.fi> 050 */ 051public class VariableSubstitutor implements Serializable 052{ 053 054 /** 055 * 056 */ 057 private static final long serialVersionUID = 3907213762447685687L; 058 059 /** The variable value mappings */ 060 protected transient Properties variables; 061 062 /** Whether braces are required for substitution. */ 063 protected boolean bracesRequired = false; 064 065 /** A constant for file type. Plain file. */ 066 protected final static int TYPE_PLAIN = 0; 067 068 /** A constant for file type. Java properties file. */ 069 protected final static int TYPE_JAVA_PROPERTIES = 1; 070 071 /** A constant for file type. XML file. */ 072 protected final static int TYPE_XML = 2; 073 074 /** A constant for file type. Shell file. */ 075 protected final static int TYPE_SHELL = 3; 076 077 /** A constant for file type. Plain file with '@' start char. */ 078 protected final static int TYPE_AT = 4; 079 080 /** A mapping of file type names to corresponding integer constants. */ 081 protected final static Map typeNameToConstantMap; 082 083 // Initialize the file type map 084 static 085 { 086 typeNameToConstantMap = new HashMap(); 087 typeNameToConstantMap.put("plain", new Integer(TYPE_PLAIN)); 088 typeNameToConstantMap.put("javaprop", new Integer(TYPE_JAVA_PROPERTIES)); 089 typeNameToConstantMap.put("xml", new Integer(TYPE_XML)); 090 typeNameToConstantMap.put("shell", new Integer(TYPE_SHELL)); 091 typeNameToConstantMap.put("at", new Integer(TYPE_AT)); 092 } 093 094 /** 095 * Constructs a new substitutor using the specified variable value mappings. The environment 096 * hashtable is copied by reference. Braces are not required by default 097 * 098 * @param variables the map with variable value mappings 099 */ 100 public VariableSubstitutor(Properties variables) 101 { 102 this.variables = variables; 103 } 104 105 /** 106 * Get whether this substitutor requires braces. 107 */ 108 public boolean areBracesRequired() 109 { 110 return bracesRequired; 111 } 112 113 /** 114 * Specify whether this substitutor requires braces. 115 */ 116 public void setBracesRequired(boolean braces) 117 { 118 bracesRequired = braces; 119 } 120 121 /** 122 * Substitutes the variables found in the specified string. Escapes special characters using 123 * file type specific escaping if necessary. 124 * 125 * @param str the string to check for variables 126 * @param type the escaping type or null for plain 127 * @return the string with substituted variables 128 * @exception IllegalArgumentException if unknown escaping type specified 129 */ 130 public String substitute(String str, String type) throws IllegalArgumentException 131 { 132 if (str == null) return null; 133 134 // Create reader and writer for the strings 135 StringReader reader = new StringReader(str); 136 StringWriter writer = new StringWriter(); 137 138 // Substitute any variables 139 try 140 { 141 substitute(reader, writer, type); 142 } 143 catch (IOException e) 144 { 145 throw new Error("Unexpected I/O exception when reading/writing memory " 146 + "buffer; nested exception is: " + e); 147 } 148 149 // Return the resulting string 150 return writer.getBuffer().toString(); 151 } 152 153 /** 154 * Substitutes the variables found in the specified input stream. Escapes special characters 155 * using file type specific escaping if necessary. 156 * 157 * @param in the input stream to read 158 * @param out the output stream to write 159 * @param type the file type or null for plain 160 * @param encoding the character encoding or null for default 161 * @exception IllegalArgumentException if unknown file type specified 162 * @exception UnsupportedEncodingException if encoding not supported 163 * @exception IOException if an I/O error occurs 164 * 165 * @return the number of substitutions made 166 */ 167 public int substitute(InputStream in, OutputStream out, String type, String encoding) 168 throws IllegalArgumentException, UnsupportedEncodingException, IOException 169 { 170 // Check if file type specific default encoding known 171 if (encoding == null) 172 { 173 int t = getTypeConstant(type); 174 switch (t) 175 { 176 case TYPE_JAVA_PROPERTIES: 177 encoding = "ISO-8859-1"; 178 break; 179 case TYPE_XML: 180 encoding = "UTF-8"; 181 break; 182 } 183 } 184 185 // Create the reader and writer 186 InputStreamReader reader = (encoding != null ? new InputStreamReader(in, encoding) 187 : new InputStreamReader(in)); 188 OutputStreamWriter writer = (encoding != null ? new OutputStreamWriter(out, encoding) 189 : new OutputStreamWriter(out)); 190 191 // Copy the data and substitute variables 192 int subs = substitute(reader, writer, type); 193 194 // Flush the writer so that everything gets written out 195 writer.flush(); 196 197 return subs; 198 } 199 200 /** 201 * Substitutes the variables found in the data read from the specified reader. Escapes special 202 * characters using file type specific escaping if necessary. 203 * 204 * @param reader the reader to read 205 * @param writer the writer used to write data out 206 * @param type the file type or null for plain 207 * @exception IllegalArgumentException if unknown file type specified 208 * @exception IOException if an I/O error occurs 209 * 210 * @return the number of substitutions made 211 */ 212 public int substitute(Reader reader, Writer writer, String type) 213 throws IllegalArgumentException, IOException 214 { 215 // Check the file type 216 int t = getTypeConstant(type); 217 218 // determine character which starts a variable 219 char variable_start = '$'; 220 if (t == TYPE_SHELL) 221 variable_start = '%'; 222 else if (t == TYPE_AT) variable_start = '@'; 223 224 int subs = 0; 225 226 // Copy data and substitute variables 227 int c = reader.read(); 228 while (true) 229 { 230 // Find the next potential variable reference or EOF 231 while (c != -1 && c != variable_start) 232 { 233 writer.write(c); 234 c = reader.read(); 235 } 236 if (c == -1) return subs; 237 238 // Check if braces used or start char escaped 239 boolean braces = false; 240 c = reader.read(); 241 if (c == '{') 242 { 243 braces = true; 244 c = reader.read(); 245 } 246 else if (bracesRequired) 247 { 248 writer.write(variable_start); 249 continue; 250 } 251 else if (c == -1) 252 { 253 writer.write(variable_start); 254 return subs; 255 } 256 257 // Read the variable name 258 StringBuffer nameBuffer = new StringBuffer(); 259 while (c != -1 && (braces && c != '}') || (c >= 'a' && c <= 'z') 260 || (c >= 'A' && c <= 'Z') || (braces && (c == '[') || (c == ']')) 261 || (((c >= '0' && c <= '9') || c == '_') && nameBuffer.length() > 0)) 262 { 263 nameBuffer.append((char) c); 264 c = reader.read(); 265 } 266 String name = nameBuffer.toString(); 267 268 // Check if a legal and defined variable found 269 String varvalue = null; 270 271 if ((!braces || c == '}') && name.length() > 0) 272 { 273 // check for environment variables 274 if (braces && name.startsWith("ENV[") 275 && (name.lastIndexOf(']') == name.length() - 1)) 276 { 277 varvalue = IoHelper.getenv(name.substring(4, name.length() - 1)); 278 } 279 else 280 varvalue = variables.getProperty(name); 281 282 subs++; 283 } 284 285 // Substitute the variable... 286 if (varvalue != null) 287 { 288 writer.write(escapeSpecialChars(varvalue, t)); 289 if (braces) c = reader.read(); 290 } 291 // ...or ignore it 292 else 293 { 294 writer.write(variable_start); 295 if (braces) writer.write('{'); 296 writer.write(name); 297 } 298 } 299 } 300 301 /** 302 * Returns the internal constant for the specified file type. 303 * 304 * @param type the type name or null for plain 305 * @return the file type constant 306 */ 307 protected int getTypeConstant(String type) 308 { 309 if (type == null) return TYPE_PLAIN; 310 Integer integer = (Integer) typeNameToConstantMap.get(type); 311 if (integer == null) 312 throw new IllegalArgumentException("Unknown file type " + type); 313 else 314 return integer.intValue(); 315 } 316 317 /** 318 * Escapes the special characters in the specified string using file type specific rules. 319 * 320 * @param str the string to check for special characters 321 * @param type the target file type (one of TYPE_xxx) 322 * @return the string with the special characters properly escaped 323 */ 324 protected String escapeSpecialChars(String str, int type) 325 { 326 StringBuffer buffer; 327 int len; 328 int i; 329 switch (type) 330 { 331 case TYPE_PLAIN: 332 case TYPE_SHELL: 333 case TYPE_AT: 334 return str; 335 case TYPE_JAVA_PROPERTIES: 336 buffer = new StringBuffer(str); 337 len = str.length(); 338 for (i = 0; i < len; i++) 339 { 340 // Check for control characters 341 char c = buffer.charAt(i); 342 if (c == '\t' || c == '\n' || c == '\r') 343 { 344 char tag; 345 if (c == '\t') 346 tag = 't'; 347 else if (c == '\n') 348 tag = 'n'; 349 else 350 tag = 'r'; 351 buffer.replace(i, i + 1, "\\" + tag); 352 len++; 353 i++; 354 } 355 356 // Check for special characters 357 if (c == '\\' || c == '"' || c == '\'' || c == ' ') 358 { 359 buffer.insert(i, '\\'); 360 len++; 361 i++; 362 } 363 } 364 return buffer.toString(); 365 case TYPE_XML: 366 buffer = new StringBuffer(str); 367 len = str.length(); 368 for (i = 0; i < len; i++) 369 { 370 String r = null; 371 char c = buffer.charAt(i); 372 switch (c) 373 { 374 case '<': 375 r = "<"; 376 break; 377 case '>': 378 r = ">"; 379 break; 380 case '&': 381 r = "&"; 382 break; 383 case '\'': 384 r = "'"; 385 break; 386 case '"': 387 r = """; 388 break; 389 } 390 if (r != null) 391 { 392 buffer.replace(i, i + 1, r); 393 len = buffer.length(); 394 i += r.length() - 1; 395 } 396 } 397 return buffer.toString(); 398 default: 399 throw new Error("Unknown file type constant " + type); 400 } 401 } 402}