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 = "&lt;";
376                    break;
377                case '>':
378                    r = "&gt;";
379                    break;
380                case '&':
381                    r = "&amp;";
382                    break;
383                case '\'':
384                    r = "&apos;";
385                    break;
386                case '"':
387                    r = "&quot;";
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}