001/** 002 * 003 * Copyright (c) 2003-2014 The Regents of the University of California. 004 * All rights reserved. 005 * 006 * Permission is hereby granted, without written agreement and without 007 * license or royalty fees, to use, copy, modify, and distribute this 008 * software and its documentation for any purpose, provided that the 009 * above copyright notice and the following two paragraphs appear in 010 * all copies of this software. 011 * 012 * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY 013 * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES 014 * ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN 015 * IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY 016 * OF SUCH DAMAGE. 017 * 018 * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES, 019 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 020 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE 021 * PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY 022 * OF CALIFORNIA HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, 023 * UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 024 */ 025 026package ptolemy.util; 027 028import java.io.BufferedReader; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.InputStreamReader; 032import java.util.Iterator; 033import java.util.LinkedHashMap; 034import java.util.Locale; 035import java.util.MissingResourceException; 036import java.util.ResourceBundle; 037 038/** 039 * Manage the resources for a locale using a set of static strings from a property file. 040 * See <code>java.util.ResourceBundle</code> for more information. 041 * 042 * <p> 043 * Unlike other types of resource bundle, <code>OrderedResourceBundle</code> is not 044 * usually subclassed. Instead, the properties files containing the resource data 045 * are supplied. <code>OrderedResourceBundle.getBundle</code> 046 * will automatically look for the appropriate properties file and create an 047 * <code>OrderedResourceBundle</code> that refers to it. See 048 * <code>java.util.ResourceBundle.getBundle()</code> for a complete description 049 * of the search and instantiation strategy. 050 * 051 * @version $Id$ 052 * @author Matthew Brooke 053 * @since Ptolemy II 8.0 054 */ 055public class OrderedResourceBundle { 056 057 /** 058 * Construct an OrderedResourceBundle. 059 * 060 * @param stream 061 * InputStream for reading the java properties file from which 062 * this object will take its values. The stream is closed 063 * by this constructor. 064 * @exception IOException 065 * if there is a problem reading the InputStream 066 * @exception NullPointerException 067 * if the InputStream is null 068 */ 069 public OrderedResourceBundle(InputStream stream) 070 throws IOException, NullPointerException { 071 072 if (stream == null) { 073 throw new NullPointerException( 074 "OrderedResourceBundle constructor received a NULL InputStream"); 075 } 076 BufferedReader propsReader = new BufferedReader( 077 new InputStreamReader(stream)); 078 079 // This method closes propsReader 080 orderedMap = getPropsAsOrderedMap(propsReader); 081 } 082 083 /////////////////////////////////////////////////////////////////// 084 // public methods 085 086 /** 087 * Get a resource bundle using the specified base name and the default 088 * locale. The returned bundle has its entries in the same order as those in 089 * the original properties file, so a call to getKeys() will return an 090 * Iterator that allows retrieval of the keys in the original order. See 091 * javadoc for <code>java.util.ResourceBundle</code> for a complete 092 * description of the search and instantiation strategy. 093 * 094 * @param baseName 095 * String denoting the name of the properties file that will be 096 * read to populate this ResourceBundle.<br> 097 * <br> Example 1: if the baseName is MyPropsFile, and the 098 * default Locale is en_US, a properties file named: 099 * <code>MyPropsFile_en_US.properties</code> will be sought on 100 * the classpath.<br> 101 * <br> Example 2: if the baseName is 102 * org.mydomain.pkg.MyPropsFile, and the default Locale is en_US, 103 * a properties file named: 104 * <code>org/mydomain/pkg/MyPropsFile_en_US.properties</code> 105 * will be sought on the classpath.<br> 106 * <br> NOTE: valid comment chars are # and !<br> 107 * <br> valid delimiters are (space) : = 108 * @return OrderedResourceBundle - a ResourceBundle with its entries in the 109 * same order as those in the original properties file 110 * @exception IOException 111 * if there is a problem reading the file 112 * @exception MissingResourceException 113 * if the file cannot be found 114 * @exception NullPointerException 115 * if baseName is null 116 */ 117 public static OrderedResourceBundle getBundle(String baseName) 118 throws IOException, MissingResourceException, NullPointerException { 119 120 String filename = getPropsFileNamePlusLocale(baseName); 121 122 InputStream stream = OrderedResourceBundle.class 123 .getResourceAsStream(filename); 124 125 // The OrderedResourceBundle closes stream. 126 return new OrderedResourceBundle(stream); 127 } 128 129 /** 130 * Get a string for the given key from this resource bundle. 131 * 132 * @param key 133 * the key for the desired string 134 * @return the string for the given key, or null if: a null value is 135 * actually mapped to this key, key not found, or key is null 136 */ 137 public String getString(String key) { 138 return (String) orderedMap.get(key); 139 } 140 141 /** 142 * Get an Iterator over the Set of keys, allowing retrieval of the keys in 143 * the original order as listed in the properties file. 144 * 145 * @return Iterator 146 */ 147 public Iterator getKeys() { 148 return orderedMap.keySet().iterator(); 149 } 150 151 /////////////////////////////////////////////////////////////////// 152 // private methods 153 154 /** Get the properties as an ordered map. 155 * @param propsReader The reader that contains the properties. This method 156 * closes propsReader upon completion 157 * @return The properties. 158 */ 159 private LinkedHashMap getPropsAsOrderedMap(BufferedReader propsReader) 160 throws IOException { 161 162 LinkedHashMap orderedMap = new LinkedHashMap(); 163 try { 164 String readLine = null; 165 166 while ((readLine = propsReader.readLine()) != null) { 167 168 readLine = readLine.trim(); 169 170 if (readLine.length() < 1) { 171 continue; 172 } 173 174 // Find start of key 175 int lineLen = readLine.length(); 176 int keyStart; 177 for (keyStart = 0; keyStart < lineLen; keyStart++) { 178 if (whiteSpaceChars 179 .indexOf(readLine.charAt(keyStart)) == -1) { 180 break; 181 } 182 } 183 184 // Continue lines that end in slashes if they are not comments 185 char firstChar = readLine.charAt(keyStart); 186 if (firstChar != '#' && firstChar != '!') { 187 while (continueLine(readLine)) { 188 String nextLine = propsReader.readLine(); 189 if (nextLine == null) { 190 nextLine = ""; 191 } 192 String choppedLine = readLine.substring(0, lineLen - 1); 193 // Advance beyond whitespace on new line 194 int startIndex; 195 for (startIndex = 0; startIndex < nextLine 196 .length(); startIndex++) { 197 if (whiteSpaceChars.indexOf( 198 nextLine.charAt(startIndex)) == -1) { 199 break; 200 } 201 } 202 nextLine = nextLine.substring(startIndex, 203 nextLine.length()); 204 readLine = choppedLine + nextLine; 205 lineLen = readLine.length(); 206 } 207 208 // Find separation between key and value 209 int sepIdx; 210 for (sepIdx = keyStart; sepIdx < lineLen; sepIdx++) { 211 char currentChar = readLine.charAt(sepIdx); 212 if (currentChar == '\\') { 213 sepIdx++; 214 } else if (keyValueSeparators 215 .indexOf(currentChar) != -1) { 216 break; 217 } 218 } 219 220 // Skip over whitespace after key if any 221 int valueIndex; 222 for (valueIndex = sepIdx; valueIndex < lineLen; valueIndex++) { 223 if (whiteSpaceChars 224 .indexOf(readLine.charAt(valueIndex)) == -1) { 225 break; 226 } 227 } 228 229 // Skip over one non whitespace key value separators if any 230 if (valueIndex < lineLen) { 231 if (strictKeyValueSeparators 232 .indexOf(readLine.charAt(valueIndex)) != -1) { 233 valueIndex++; 234 } 235 } 236 // Skip over white space after other separators if any 237 while (valueIndex < lineLen) { 238 if (whiteSpaceChars 239 .indexOf(readLine.charAt(valueIndex)) == -1) { 240 break; 241 } 242 valueIndex++; 243 } 244 String nextKey = readLine.substring(keyStart, sepIdx); 245 String nextVal = sepIdx < lineLen 246 ? readLine.substring(valueIndex, lineLen) 247 : ""; 248 orderedMap.put(unescape(nextKey), unescape(nextVal)); 249 } 250 } 251 } finally { 252 try { 253 if (propsReader != null) { 254 propsReader.close(); 255 } 256 } catch (IOException ce) { 257 } 258 } 259 return orderedMap; 260 } 261 262 // un-escape all the escaped standard delimiters and comment chars, if any 263 // exist. Works for " " : # = ! 264 private String unescape(String line) { 265 266 line = line.replaceAll("\\\\ ", " "); 267 line = line.replaceAll("\\\\:", ":"); 268 line = line.replaceAll("\\\\#", "#"); 269 line = line.replaceAll("\\\\=", "="); 270 line = line.replaceAll("\\\\!", "!"); 271 272 return line; 273 } 274 275 /* 276 * Returns true if the given line is a line that must be appended to the 277 * next line 278 */ 279 private boolean continueLine(String line) { 280 int slashCount = 0; 281 int index = line.length() - 1; 282 while (index >= 0 && line.charAt(index--) == '\\') { 283 slashCount++; 284 } 285 // FindBugs: The code uses x % 2 == 1 to check to see if a value is odd, 286 // but this won't work for negative numbers (e.g., (-5) % 2 == -1). 287 // If this code is intending to check for oddness, consider 288 // using x & 1 == 1, or x % 2 != 0. 289 290 return slashCount % 2 != 0; 291 } 292 293 private static String getPropsFileNamePlusLocale(String baseName) 294 throws MissingResourceException, NullPointerException { 295 296 if (baseName == null) { 297 return null; 298 } 299 baseName = baseName.trim(); 300 301 if (baseName.length() < 1) { 302 return baseName; 303 } 304 305 // use ResourceBundle's code to find the properties file's locale 306 // - ie the last part of its name - such as the "_en_US" at the end 307 // of "mypropsfile_en_US.properties" 308 Locale bundleLocale = ResourceBundle.getBundle(baseName).getLocale(); 309 310 String lang = bundleLocale.getLanguage(); 311 String ctry = bundleLocale.getCountry(); 312 String vart = bundleLocale.getVariant(); 313 314 boolean hasLang = lang.length() > 0; 315 boolean hasCtry = ctry.length() > 0; 316 boolean hasVart = vart.length() > 0; 317 318 baseName = baseName.replace('.', '/'); 319 320 if (!baseName.startsWith(FWD_SLASH)) { 321 baseName = FWD_SLASH + baseName; 322 } 323 324 StringBuffer fnBuff = new StringBuffer(baseName); 325 326 if (!hasLang && !hasCtry && !hasVart) { 327 fnBuff.append(PROPS_EXT); 328 return fnBuff.toString(); 329 } 330 331 if (hasLang) { 332 fnBuff.append(UNDERSCORE); 333 fnBuff.append(lang); 334 } 335 if (hasCtry) { 336 fnBuff.append(UNDERSCORE); 337 fnBuff.append(ctry); 338 } 339 if (hasVart) { 340 fnBuff.append(UNDERSCORE); 341 fnBuff.append(vart); 342 } 343 fnBuff.append(PROPS_EXT); 344 345 return fnBuff.toString(); 346 } 347 348 /////////////////////////////////////////////////////////////////// 349 // private variables 350 351 private final static String FWD_SLASH = "/"; 352 private final static String UNDERSCORE = "_"; 353 private final static String PROPS_EXT = ".properties"; 354 355 private static final String whiteSpaceChars = " \t\r\n\f"; 356 private static final String keyValueSeparators = "=: \t\r\n\f"; 357 private static final String strictKeyValueSeparators = "=:"; 358 359 private LinkedHashMap orderedMap; 360}