001: /*
002: * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
003: *
004: * http://izpack.org/
005: * http://izpack.codehaus.org/
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:
022: package com.izforge.izpack.util;
023:
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.io.InputStreamReader;
027: import java.io.OutputStream;
028: import java.io.OutputStreamWriter;
029: import java.io.Reader;
030: import java.io.Serializable;
031: import java.io.StringReader;
032: import java.io.StringWriter;
033: import java.io.UnsupportedEncodingException;
034: import java.io.Writer;
035: import java.util.HashMap;
036: import java.util.Map;
037: import 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: */
051: public class VariableSubstitutor implements Serializable {
052:
053: /**
054: *
055: */
056: private static final long serialVersionUID = 3907213762447685687L;
057:
058: /** The variable value mappings */
059: protected transient Properties variables;
060:
061: /** Whether braces are required for substitution. */
062: protected boolean bracesRequired = false;
063:
064: /** A constant for file type. Plain file. */
065: protected final static int TYPE_PLAIN = 0;
066:
067: /** A constant for file type. Java properties file. */
068: protected final static int TYPE_JAVA_PROPERTIES = 1;
069:
070: /** A constant for file type. XML file. */
071: protected final static int TYPE_XML = 2;
072:
073: /** A constant for file type. Shell file. */
074: protected final static int TYPE_SHELL = 3;
075:
076: /** A constant for file type. Plain file with '@' start char. */
077: protected final static int TYPE_AT = 4;
078:
079: /** A constant for file type. Java file, where \ have to be escaped. */
080: protected final static int TYPE_JAVA = 5;
081:
082: /** A constant for file type. Plain file with ANT-like variable markers, ie @param@ */
083: protected final static int TYPE_ANT = 6;
084:
085: /** PLAIN = "plain" */
086: public final static String PLAIN = "plain";
087:
088: /** A mapping of file type names to corresponding integer constants. */
089: protected final static Map<String, Integer> typeNameToConstantMap;
090:
091: // Initialize the file type map
092: static {
093: typeNameToConstantMap = new HashMap<String, Integer>();
094: typeNameToConstantMap.put("plain", TYPE_PLAIN);
095: typeNameToConstantMap.put("javaprop", TYPE_JAVA_PROPERTIES);
096: typeNameToConstantMap.put("java", TYPE_JAVA);
097: typeNameToConstantMap.put("xml", TYPE_XML);
098: typeNameToConstantMap.put("shell", TYPE_SHELL);
099: typeNameToConstantMap.put("at", TYPE_AT);
100: typeNameToConstantMap.put("ant", TYPE_ANT);
101: }
102:
103: /**
104: * Constructs a new substitutor using the specified variable value mappings. The environment
105: * hashtable is copied by reference. Braces are not required by default
106: *
107: * @param variables the map with variable value mappings
108: */
109: public VariableSubstitutor(Properties variables) {
110: this .variables = variables;
111: }
112:
113: /**
114: * Get whether this substitutor requires braces.
115: */
116: public boolean areBracesRequired() {
117: return bracesRequired;
118: }
119:
120: /**
121: * Specify whether this substitutor requires braces.
122: */
123: public void setBracesRequired(boolean braces) {
124: bracesRequired = braces;
125: }
126:
127: /**
128: * Substitutes the variables found in the specified string. Escapes special characters using
129: * file type specific escaping if necessary.
130: *
131: * @param str the string to check for variables
132: * @param type the escaping type or null for plain
133: * @return the string with substituted variables
134: * @exception IllegalArgumentException if unknown escaping type specified
135: */
136: public String substitute(String str, String type)
137: throws IllegalArgumentException {
138: if (str == null)
139: return null;
140:
141: // Create reader and writer for the strings
142: StringReader reader = new StringReader(str);
143: StringWriter writer = new StringWriter();
144:
145: // Substitute any variables
146: try {
147: substitute(reader, writer, type);
148: } catch (IOException e) {
149: throw new Error(
150: "Unexpected I/O exception when reading/writing memory "
151: + "buffer; nested exception is: " + e);
152: }
153:
154: // Return the resulting string
155: return writer.getBuffer().toString();
156: }
157:
158: /**
159: * Substitutes the variables found in the specified input stream. Escapes special characters
160: * using file type specific escaping if necessary.
161: *
162: * @param in the input stream to read
163: * @param out the output stream to write
164: * @param type the file type or null for plain
165: * @param encoding the character encoding or null for default
166: * @exception IllegalArgumentException if unknown file type specified
167: * @exception UnsupportedEncodingException if encoding not supported
168: * @exception IOException if an I/O error occurs
169: *
170: * @return the number of substitutions made
171: */
172: public int substitute(InputStream in, OutputStream out,
173: String type, String encoding)
174: throws IllegalArgumentException,
175: UnsupportedEncodingException, IOException {
176: // Check if file type specific default encoding known
177: if (encoding == null) {
178: int t = getTypeConstant(type);
179: switch (t) {
180: case TYPE_JAVA_PROPERTIES:
181: encoding = "ISO-8859-1";
182: break;
183: case TYPE_XML:
184: encoding = "UTF-8";
185: break;
186: }
187: }
188:
189: // Create the reader and writer
190: InputStreamReader reader = (encoding != null ? new InputStreamReader(
191: in, encoding)
192: : new InputStreamReader(in));
193: OutputStreamWriter writer = (encoding != null ? new OutputStreamWriter(
194: out, encoding)
195: : new OutputStreamWriter(out));
196:
197: // Copy the data and substitute variables
198: int subs = substitute(reader, writer, type);
199:
200: // Flush the writer so that everything gets written out
201: writer.flush();
202:
203: return subs;
204: }
205:
206: /**
207: * Substitute method Variant that gets An Input Stream and returns A String
208: *
209: * @param in The Input Stream, with Placeholders
210: * @param type The used FormatType
211: *
212: * @throws IllegalArgumentException If a wrong input was given.
213: * @throws UnsupportedEncodingException If the file comes with a wrong Encoding
214: * @throws IOException If an I/O Error occurs.
215: *
216: * @return the substituted result as string
217: */
218: public String substitute(InputStream in, String type)
219: throws IllegalArgumentException,
220: UnsupportedEncodingException, IOException {
221: // Check if file type specific default encoding known
222: String encoding = PLAIN;
223: {
224: int t = getTypeConstant(type);
225:
226: switch (t) {
227: case TYPE_JAVA_PROPERTIES:
228: encoding = "ISO-8859-1";
229:
230: break;
231:
232: case TYPE_XML:
233: encoding = "UTF-8";
234:
235: break;
236: }
237: }
238:
239: // Create the reader and writer
240: InputStreamReader reader = ((encoding != null) ? new InputStreamReader(
241: in, encoding)
242: : new InputStreamReader(in));
243: StringWriter writer = new StringWriter();
244:
245: // Copy the data and substitute variables
246: substitute(reader, writer, type);
247:
248: // Flush the writer so that everything gets written out
249: writer.flush();
250:
251: return writer.getBuffer().toString();
252: }
253:
254: /**
255: * Substitutes the variables found in the data read from the specified reader. Escapes special
256: * characters using file type specific escaping if necessary.
257: *
258: * @param reader the reader to read
259: * @param writer the writer used to write data out
260: * @param type the file type or null for plain
261: * @exception IllegalArgumentException if unknown file type specified
262: * @exception IOException if an I/O error occurs
263: *
264: * @return the number of substitutions made
265: */
266: public int substitute(Reader reader, Writer writer, String type)
267: throws IllegalArgumentException, IOException {
268: // Check the file type
269: int t = getTypeConstant(type);
270:
271: // determine character which starts (and ends) a variable
272: char variable_start = '$';
273: char variable_end = '\0';
274: if (t == TYPE_SHELL)
275: variable_start = '%';
276: else if (t == TYPE_AT)
277: variable_start = '@';
278: else if (t == TYPE_ANT) {
279: variable_start = '@';
280: variable_end = '@';
281: }
282:
283: int subs = 0;
284:
285: // Copy data and substitute variables
286: int c = reader.read();
287:
288: // Ignore BOM of UTF-8
289: if (c == 0xEF) {
290: for (int i = 0; i < 2; i++) {
291: c = reader.read();
292: }
293: }
294: // Ignore quaint return values at UTF-8 BOMs.
295: if (c > 0xFF)
296: c = reader.read();
297: while (true) {
298: // Find the next potential variable reference or EOF
299: while (c != -1 && c != variable_start) {
300: writer.write(c);
301: c = reader.read();
302: }
303: if (c == -1)
304: return subs;
305:
306: // Check if braces used or start char escaped
307: boolean braces = false;
308: c = reader.read();
309: if (c == '{') {
310: braces = true;
311: c = reader.read();
312: } else if (bracesRequired) {
313: writer.write(variable_start);
314: continue;
315: } else if (c == -1) {
316: writer.write(variable_start);
317: return subs;
318: }
319:
320: // Read the variable name
321: StringBuffer nameBuffer = new StringBuffer();
322: while (c != -1
323: && (braces && c != '}')
324: || (c >= 'a' && c <= 'z')
325: || (c >= 'A' && c <= 'Z')
326: || (braces && (c == '[') || (c == ']'))
327: || (((c >= '0' && c <= '9') || c == '_' || c == '.' || c == '-') && nameBuffer
328: .length() > 0)) {
329: nameBuffer.append((char) c);
330: c = reader.read();
331: }
332: String name = nameBuffer.toString();
333:
334: // Check if a legal and defined variable found
335: String varvalue = null;
336:
337: if (((!braces || c == '}') && (!braces
338: || variable_end == '\0' || variable_end == c))
339: && name.length() > 0) {
340: // check for environment variables
341: if (braces && name.startsWith("ENV[")
342: && (name.lastIndexOf(']') == name.length() - 1)) {
343: varvalue = IoHelper.getenv(name.substring(4, name
344: .length() - 1));
345: } else
346: varvalue = variables.getProperty(name);
347:
348: subs++;
349: }
350:
351: // Substitute the variable...
352: if (varvalue != null) {
353: writer.write(escapeSpecialChars(varvalue, t));
354: if (braces || variable_end != '\0')
355: c = reader.read();
356: }
357: // ...or ignore it
358: else {
359: writer.write(variable_start);
360: if (braces)
361: writer.write('{');
362: writer.write(name);
363: }
364: }
365: }
366:
367: /**
368: * Returns the internal constant for the specified file type.
369: *
370: * @param type the type name or null for plain
371: * @return the file type constant
372: */
373: protected int getTypeConstant(String type) {
374: if (type == null)
375: return TYPE_PLAIN;
376: Integer integer = typeNameToConstantMap.get(type);
377: if (integer == null)
378: throw new IllegalArgumentException("Unknown file type "
379: + type);
380: else
381: return integer;
382: }
383:
384: /**
385: * Escapes the special characters in the specified string using file type specific rules.
386: *
387: * @param str the string to check for special characters
388: * @param type the target file type (one of TYPE_xxx)
389: * @return the string with the special characters properly escaped
390: */
391: protected String escapeSpecialChars(String str, int type) {
392: StringBuffer buffer;
393: int len;
394: int i;
395: switch (type) {
396: case TYPE_PLAIN:
397: case TYPE_SHELL:
398: case TYPE_AT:
399: case TYPE_ANT:
400: return str;
401: case TYPE_JAVA_PROPERTIES:
402: case TYPE_JAVA:
403: buffer = new StringBuffer(str);
404: len = str.length();
405: for (i = 0; i < len; i++) {
406: // Check for control characters
407: char c = buffer.charAt(i);
408: if (type == TYPE_JAVA_PROPERTIES) {
409: if (c == '\t' || c == '\n' || c == '\r') {
410: char tag;
411: if (c == '\t')
412: tag = 't';
413: else if (c == '\n')
414: tag = 'n';
415: else
416: tag = 'r';
417: buffer.replace(i, i + 1, "\\" + tag);
418: len++;
419: i++;
420: }
421:
422: // Check for special characters
423: if (c == '\\' || c == '"' || c == '\'' || c == ' ') {
424: buffer.insert(i, '\\');
425: len++;
426: i++;
427: }
428: } else {
429: if (c == '\\') {
430: buffer.replace(i, i + 1, "\\\\");
431: len++;
432: i++;
433: }
434: }
435: }
436: return buffer.toString();
437: case TYPE_XML:
438: buffer = new StringBuffer(str);
439: len = str.length();
440: for (i = 0; i < len; i++) {
441: String r = null;
442: char c = buffer.charAt(i);
443: switch (c) {
444: case '<':
445: r = "<";
446: break;
447: case '>':
448: r = ">";
449: break;
450: case '&':
451: r = "&";
452: break;
453: case '\'':
454: r = "'";
455: break;
456: case '"':
457: r = """;
458: break;
459: }
460: if (r != null) {
461: buffer.replace(i, i + 1, r);
462: len = buffer.length();
463: i += r.length() - 1;
464: }
465: }
466: return buffer.toString();
467: default:
468: throw new Error("Unknown file type constant " + type);
469: }
470: }
471: }
|