| /* |
| * 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; |
| |
| /** |
| * <p> |
| * 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. |
| * </p> |
| * |
| * <p> |
| * This class was pulled out of Jakarta Commons Configuration and |
| * Jakarta Commons Lang trunk revision 476093 |
| * </p> |
| */ |
| 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('\\'); |
| } |
| } |
| } |