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}