001 // Copyright 2004, 2005 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry.util.text;
016
017 import java.io.BufferedReader;
018 import java.io.IOException;
019 import java.io.InputStream;
020 import java.io.InputStreamReader;
021 import java.io.Reader;
022 import java.io.UnsupportedEncodingException;
023 import java.util.Map;
024
025 /**
026 * An object that loads a properties file from the provided input stream or
027 * reader. This class reads the property file exactly like java.util.Properties,
028 * except that it also allows the files to use an encoding other than ISO-8859-1
029 * and all non-ASCII characters are read correctly using the given encoding. In
030 * short, non-latin characters no longer need to be quoted using native2ascii.
031 *
032 * @author mb
033 * @since 4.0
034 */
035 public class LocalizedPropertiesLoader
036 {
037
038 private static final String HEX_DIGITS = "0123456789ABCDEF";
039
040 private static final ICharacterMatcher WHITESPACE = new WhitespaceMatcher(
041 false);
042 private static final ICharacterMatcher LINE_SEPARATOR = new AsciiCharacterMatcher(
043 "\n\r");
044 private static final ICharacterMatcher NOT_LINE_SEPARATOR = new InverseMatcher(
045 LINE_SEPARATOR);
046 private static final ICharacterMatcher KEY_VALUE_SEPARATOR = new AsciiCharacterMatcher(
047 "=:");
048 private static final ICharacterMatcher SEPARATOR = new AsciiCharacterMatcher(
049 "=:\r\n");
050 private static final ICharacterMatcher COMMENT = new AsciiCharacterMatcher(
051 "#!");
052 private static final ICharacterMatcher WHITESPACE_OR_SEPARATOR = new CompoundMatcher(
053 new ICharacterMatcher[] { WHITESPACE, SEPARATOR });
054
055 private ExtendedReader _extendedReader;
056
057 /**
058 * Creates a new loader that will load the properties from the given input
059 * stream using the default character encoding.
060 *
061 * @param ins
062 * the input stream to load the properties from
063 */
064 public LocalizedPropertiesLoader(InputStream ins)
065 {
066 this(new InputStreamReader(ins));
067 }
068
069 /**
070 * Creates a new loader that will load the properties from the given input
071 * stream using the provided character encoding.
072 *
073 * @param ins
074 * the input stream to load the properties from
075 * @param encoding
076 * the character encoding the be used when reading from the
077 * stream
078 * @throws UnsupportedEncodingException
079 * if the name of the encoding cannot be recognized
080 */
081 public LocalizedPropertiesLoader(InputStream ins, String encoding)
082 throws UnsupportedEncodingException
083 {
084 this(new InputStreamReader(ins, encoding));
085 }
086
087 /**
088 * Creates a new loader that will load the properties from the given reader.
089 *
090 * @param reader
091 * the Reader to load the properties from
092 */
093 public LocalizedPropertiesLoader(Reader reader)
094 {
095 _extendedReader = new ExtendedReader(new BufferedReader(reader));
096 }
097
098 /**
099 * Read the properties from the provided stream and store them into the
100 * given map.
101 *
102 * @param properties
103 * the map where the properties will be stored
104 * @throws IOException
105 * if an error occurs
106 */
107 public void load(Map properties)
108 throws IOException
109 {
110 while(!isAtEndOfStream())
111 {
112 // we are at the beginning of a line.
113 // check whether it is a comment and if it is, skip it
114 int nextChar = _extendedReader.peek();
115 if (COMMENT.matches((char) nextChar))
116 {
117 _extendedReader.skipCharacters(NOT_LINE_SEPARATOR);
118 continue;
119 }
120
121 _extendedReader.skipCharacters(WHITESPACE);
122 if (!isAtEndOfLine())
123 {
124 // this line does not consist only of whitespace. the next word
125 // is the key
126 String key = readQuotedLine(WHITESPACE_OR_SEPARATOR);
127 _extendedReader.skipCharacters(WHITESPACE);
128
129 // if the next char is a key-value separator, read it and skip
130 // the following spaces
131 nextChar = _extendedReader.peek();
132 if (nextChar > 0
133 && KEY_VALUE_SEPARATOR.matches((char) nextChar))
134 {
135 _extendedReader.read();
136 _extendedReader.skipCharacters(WHITESPACE);
137 }
138
139 // finally, read the value
140 String value = readQuotedLine(LINE_SEPARATOR);
141
142 properties.put(key, value);
143 }
144 _extendedReader.skipCharacters(LINE_SEPARATOR);
145 }
146 }
147
148 private boolean isAtEndOfStream()
149 throws IOException
150 {
151 int nextChar = _extendedReader.peek();
152 return (nextChar < 0);
153 }
154
155 private boolean isAtEndOfLine()
156 throws IOException
157 {
158 int nextChar = _extendedReader.peek();
159 if (nextChar < 0) return true;
160 return LINE_SEPARATOR.matches((char) nextChar);
161 }
162
163 private String readQuotedLine(ICharacterMatcher terminators)
164 throws IOException
165 {
166 StringBuffer buf = new StringBuffer();
167
168 while(true)
169 {
170 // see what the next char is
171 int nextChar = _extendedReader.peek();
172
173 // if at end of stream or the char is one of the terminators, stop
174 if (nextChar < 0 || terminators.matches((char) nextChar)) break;
175
176 try
177 {
178 // read the char (and possibly unquote it)
179 char ch = readQuotedChar();
180 buf.append(ch);
181 }
182 catch (IgnoreCharacterException e)
183 {
184 // simply ignore -- no character was read
185 }
186 }
187
188 return buf.toString();
189 }
190
191 private char readQuotedChar()
192 throws IOException, IgnoreCharacterException
193 {
194 int nextChar = _extendedReader.read();
195 if (nextChar < 0) throw new IgnoreCharacterException();
196 char ch = (char) nextChar;
197
198 // if the char is not the quotation char, simply return it
199 if (ch != '\\') return ch;
200
201 // the character is a quotation character. unquote it
202 nextChar = _extendedReader.read();
203
204 // if at the end of the stream, stop
205 if (nextChar < 0) throw new IgnoreCharacterException();
206
207 ch = (char) nextChar;
208 switch(ch)
209 {
210 case 'u':
211 char res = 0;
212 for(int i = 0; i < 4; i++)
213 {
214 nextChar = _extendedReader.read();
215 if (nextChar < 0)
216 throw new IllegalArgumentException(
217 "Malformed \\uxxxx encoding.");
218 char digitChar = (char) nextChar;
219 int digit = HEX_DIGITS
220 .indexOf(Character.toUpperCase(digitChar));
221 if (digit < 0)
222 throw new IllegalArgumentException(
223 "Malformed \\uxxxx encoding.");
224 res = (char) (res * 16 + digit);
225 }
226 return res;
227
228 case '\r':
229 // if the next char is \n, read it and fall through
230 nextChar = _extendedReader.peek();
231 if (nextChar == '\n') _extendedReader.read();
232 case '\n':
233 _extendedReader.skipCharacters(WHITESPACE);
234 throw new IgnoreCharacterException();
235
236 case 't':
237 return '\t';
238 case 'n':
239 return '\n';
240 case 'r':
241 return '\r';
242 default:
243 return ch;
244 }
245 }
246
247 /**
248 *
249 * @author unknown
250 */
251 private static class IgnoreCharacterException extends Exception
252 {
253
254 private static final long serialVersionUID = 8366308710256427596L;
255 }
256 }