/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.opensymphony.xwork2.util; import java.io.*; import java.util.ArrayList; import java.util.List; /** * This class is used to read properties lines. These lines do * not terminate with new-line chars but rather when there is no * backslash sign a the end of the line. This is used to * concatenate multiple lines for readability. * * This class was pulled out of Jakarta Commons Configuration and * Jakarta Commons Lang trunk revision 476093 */ public class PropertiesReader extends LineNumberReader { /** Stores the comment lines for the currently processed property.*/ private List<String> commentLines; /** Stores the name of the last read property.*/ private String propertyName; /** Stores the value of the last read property.*/ private String propertyValue; /** Stores the list delimiter character.*/ private char delimiter; /** Constant for the supported comment characters.*/ static final String COMMENT_CHARS = "#!"; /** Constant for the radix of hex numbers.*/ private static final int HEX_RADIX = 16; /** Constant for the length of a unicode literal.*/ private static final int UNICODE_LEN = 4; /** The list of possible key/value separators */ private static final char[] SEPARATORS = new char[] {'=', ':'}; /** The white space characters used as key/value separators. */ private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'}; /** * Constructor. * * @param reader A Reader. */ public PropertiesReader(Reader reader) { this(reader, ','); } /** * Creates a new instance of <code>PropertiesReader</code> and sets * the underlaying reader and the list delimiter. * * @param reader the reader * @param listDelimiter the list delimiter character * @since 1.3 */ public PropertiesReader(Reader reader, char listDelimiter) { super(reader); commentLines = new ArrayList<String>(); delimiter = listDelimiter; } /** * Tests whether a line is a comment, i.e. whether it starts with a comment * character. * * @param line the line * @return a flag if this is a comment line * @since 1.3 */ boolean isCommentLine(String line) { String s = line.trim(); // blanc lines are also treated as comment lines return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0; } /** * Reads a property line. Returns null if Stream is * at EOF. Concatenates lines ending with "\". * Skips lines beginning with "#" or "!" and empty lines. * The return value is a property definition (<code><name></code> * = <code><value></code>) * * @return A string containing a property value or null * * @throws IOException in case of an I/O error */ public String readProperty() throws IOException { commentLines.clear(); StringBuilder buffer = new StringBuilder(); while (true) { String line = readLine(); if (line == null) { // EOF return null; } if (isCommentLine(line)) { commentLines.add(line); continue; } line = line.trim(); if (checkCombineLines(line)) { line = line.substring(0, line.length() - 1); buffer.append(line); } else { buffer.append(line); break; } } return buffer.toString(); } /** * Parses the next property from the input stream and stores the found * name and value in internal fields. These fields can be obtained using * the provided getter methods. The return value indicates whether EOF * was reached (<b>false</b>) or whether further properties are * available (<b>true</b>). * * @return a flag if further properties are available * @throws IOException if an error occurs * @since 1.3 */ public boolean nextProperty() throws IOException { String line = readProperty(); if (line == null) { return false; // EOF } // parse the line String[] property = parseProperty(line); propertyName = unescapeJava(property[0]); propertyValue = unescapeJava(property[1], delimiter); return true; } /** * Returns the comment lines that have been read for the last property. * * @return the comment lines for the last property returned by * <code>readProperty()</code> * @since 1.3 */ public List<String> getCommentLines() { return commentLines; } /** * Returns the name of the last read property. This method can be called * after <code>{@link #nextProperty()}</code> was invoked and its * return value was <b>true</b>. * * @return the name of the last read property * @since 1.3 */ public String getPropertyName() { return propertyName; } /** * Returns the value of the last read property. This method can be * called after <code>{@link #nextProperty()}</code> was invoked and * its return value was <b>true</b>. * * @return the value of the last read property * @since 1.3 */ public String getPropertyValue() { return propertyValue; } /** * Checks if the passed in line should be combined with the following. * This is true, if the line ends with an odd number of backslashes. * * @param line the line * @return a flag if the lines should be combined */ private boolean checkCombineLines(String line) { int bsCount = 0; for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--) { bsCount++; } return bsCount % 2 == 1; } /** * Parse a property line and return the key and the value in an array. * * @param line the line to parse * @return an array with the property's key and value * @since 1.2 */ private String[] parseProperty(String line) { // sorry for this spaghetti code, please replace it as soon as // possible with a regexp when the Java 1.3 requirement is dropped String[] result = new String[2]; StringBuilder key = new StringBuilder(); StringBuilder value = new StringBuilder(); // state of the automaton: // 0: key parsing // 1: antislash found while parsing the key // 2: separator crossing // 3: value parsing int state = 0; for (int pos = 0; pos < line.length(); pos++) { char c = line.charAt(pos); switch (state) { case 0: if (c == '\\') { state = 1; } else if (contains(WHITE_SPACE, c)) { // switch to the separator crossing state state = 2; } else if (contains(SEPARATORS, c)) { // switch to the value parsing state state = 3; } else { key.append(c); } break; case 1: if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c)) { // this is an escaped separator or white space key.append(c); } else { // another escaped character, the '\' is preserved key.append('\\'); key.append(c); } // return to the key parsing state state = 0; break; case 2: if (contains(WHITE_SPACE, c)) { // do nothing, eat all white spaces state = 2; } else if (contains(SEPARATORS, c)) { // switch to the value parsing state state = 3; } else { // any other character indicates we encoutered the beginning of the value value.append(c); // switch to the value parsing state state = 3; } break; case 3: value.append(c); break; } } result[0] = key.toString().trim(); result[1] = value.toString().trim(); return result; } /** * <p>Unescapes any Java literals found in the <code>String</code> to a * <code>Writer</code>.</p> This is a slightly modified version of the * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't * drop escaped separators (i.e '\,'). * * @param str the <code>String</code> to unescape, may be null * @param delimiter the delimiter for multi-valued properties * @return the processed string * @throws IllegalArgumentException if the Writer is <code>null</code> */ protected static String unescapeJava(String str, char delimiter) { if (str == null) { return null; } int sz = str.length(); StringBuilder out = new StringBuilder(sz); StringBuffer unicode = new StringBuffer(UNICODE_LEN); boolean hadSlash = false; boolean inUnicode = false; for (int i = 0; i < sz; i++) { char ch = str.charAt(i); if (inUnicode) { // if in unicode, then we're reading unicode // values in somehow unicode.append(ch); if (unicode.length() == UNICODE_LEN) { // unicode now contains the four hex digits // which represents our unicode character try { int value = Integer.parseInt(unicode.toString(), HEX_RADIX); out.append((char) value); unicode.setLength(0); inUnicode = false; hadSlash = false; } catch (NumberFormatException nfe) { throw new RuntimeException("Unable to parse unicode value: " + unicode, nfe); } } continue; } if (hadSlash) { // handle an escaped value hadSlash = false; if (ch == '\\') { out.append('\\'); } else if (ch == '\'') { out.append('\''); } else if (ch == '\"') { out.append('"'); } else if (ch == 'r') { out.append('\r'); } else if (ch == 'f') { out.append('\f'); } else if (ch == 't') { out.append('\t'); } else if (ch == 'n') { out.append('\n'); } else if (ch == 'b') { out.append('\b'); } else if (ch == delimiter) { out.append('\\'); out.append(delimiter); } else if (ch == 'u') { // uh-oh, we're in unicode country.... inUnicode = true; } else { out.append(ch); } continue; } else if (ch == '\\') { hadSlash = true; continue; } out.append(ch); } if (hadSlash) { // then we're in the weird case of a \ at the end of the // string, let's output it anyway. out.append('\\'); } return out.toString(); } /** * <p>Checks if the object is in the given array.</p> * * <p>The method returns <code>false</code> if a <code>null</code> array is passed in.</p> * * @param array the array to search through * @param objectToFind the object to find * @return <code>true</code> if the array contains the object */ public boolean contains(char[] array, char objectToFind) { if (array == null) { return false; } for (char anArray : array) { if (objectToFind == anArray) { return true; } } return false; } /** * <p>Unescapes any Java literals found in the <code>String</code>. * For example, it will turn a sequence of <code>'\'</code> and * <code>'n'</code> into a newline character, unless the <code>'\'</code> * is preceded by another <code>'\'</code>.</p> * * @param str the <code>String</code> to unescape, may be null * @return a new unescaped <code>String</code>, <code>null</code> if null string input */ public static String unescapeJava(String str) { if (str == null) { return null; } try { StringWriter writer = new StringWriter(str.length()); unescapeJava(writer, str); return writer.toString(); } catch (IOException ioe) { // this should never ever happen while writing to a StringWriter ioe.printStackTrace(); return null; } } /** * <p>Unescapes any Java literals found in the <code>String</code> to a * <code>Writer</code>.</p> * * <p>For example, it will turn a sequence of <code>'\'</code> and * <code>'n'</code> into a newline character, unless the <code>'\'</code> * is preceded by another <code>'\'</code>.</p> * * <p>A <code>null</code> string input has no effect.</p> * * @param out the <code>Writer</code> used to output unescaped characters * @param str the <code>String</code> to unescape, may be null * @throws IllegalArgumentException if the Writer is <code>null</code> * @throws IOException if error occurs on underlying Writer */ public static void unescapeJava(Writer out, String str) throws IOException { if (out == null) { throw new IllegalArgumentException("The Writer must not be null"); } if (str == null) { return; } int sz = str.length(); StringBuffer unicode = new StringBuffer(4); boolean hadSlash = false; boolean inUnicode = false; for (int i = 0; i < sz; i++) { char ch = str.charAt(i); if (inUnicode) { // if in unicode, then we're reading unicode // values in somehow unicode.append(ch); if (unicode.length() == 4) { // unicode now contains the four hex digits // which represents our unicode character try { int value = Integer.parseInt(unicode.toString(), 16); out.write((char) value); unicode.setLength(0); inUnicode = false; hadSlash = false; } catch (NumberFormatException nfe) { throw new RuntimeException("Unable to parse unicode value: " + unicode, nfe); } } continue; } if (hadSlash) { // handle an escaped value hadSlash = false; switch (ch) { case '\\': out.write('\\'); break; case '\'': out.write('\''); break; case '\"': out.write('"'); break; case 'r': out.write('\r'); break; case 'f': out.write('\f'); break; case 't': out.write('\t'); break; case 'n': out.write('\n'); break; case 'b': out.write('\b'); break; case 'u': { // uh-oh, we're in unicode country.... inUnicode = true; break; } default : out.write(ch); break; } continue; } else if (ch == '\\') { hadSlash = true; continue; } out.write(ch); } if (hadSlash) { // then we're in the weird case of a \ at the end of the // string, let's output it anyway. out.write('\\'); } } }