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 }