package com.idega.core.ldap.client.cbutil;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Vector;
/**
* Java's PropertyResourceBundle class is tragically
* bad. Why a class intended for i18n use deliberately
* restricts itself to 8859-1 characters, while using
* a bizarre unicode escaping format rather than utf8,
* is beyond me (and
* certainly beyond any translators I need to send
* stuff to.)<p>
*
* This class is a reimplementation that automatically
* selects whether a file is 16bit unicode, utf-8, or
* local character encoding, and loads it accordingly.
* Otherwise it is intended to be functionally very
* similar to PropertiesResourceBundle.<p>
*
* Note that this class does <i>not</i> extend ResourceBundle,
* as ResourceBundle is difficult to
* extend - all the functionality is hidden away
* in private methods. So even though ResourceBundle
* does some neat things, we won't be doing anything but a
* bare bones rewrite here.
*
* <B>Important:</B> This uses a simplified form of the properties
* file - keys and phrases are separated by an '=' sign, and while,
* for backward compatibility, keys <i>can</i> have escaped characters,
* except for '=', they don't have to. A further restriction is that
* the '=' sign <i>may not</i> be immediately preceeded by an escaped
* escape character (i.e. '\\=' is illegal) - the '=' sign must be
* preceeded by a space character in this case. Any space characters
* at the start and end of a key/description are trimmed.<p>
*
* e.g. [values in square brackets represent byte values within the file]
*
* potato = kartoffel // normal ascii (german)
* help = [30 d8 30 eb 30d7] // 16 bit unicode (japanese)
* file = [e6 96 87 e6 a1 a3] // utf-8 (chinese)
*
*/
public class CBResourceBundle
{
Hashtable translations = new Hashtable();
/**
* This creates a Resource bundle using only the name of the
* the resource bundle (e.g. "language.JX"). It then uses
* the default locale and resource loader to track down the
* appropriate translation file.
* @param baseName the name of the translation file to look up.
* this name is extended using the standard locality rules
* to try to find localised files (e.g. "language.JX" becomes
* language/JX_fr_CA in french-speaking canada).
*/
public CBResourceBundle(String baseName)
{
loadBundle(baseName, Locale.getDefault(), ClassLoader.getSystemClassLoader());
}
/**
* This creates a Resource bundle the name of the
* the resource bundle (e.g. "language.JX"). It then uses
* the specified locale to track down the
* appropriate translation file.
* @param baseName the name of the translation file to look up.
* this name is extended using the standard locality rules
* to try to find localised files (e.g. "language.JX" becomes
* language/JX_fr_CA in french-speaking canada).
* @param locale a specific locale to use in place of the default
* system locale.
*/
public CBResourceBundle(String baseName, Locale locale)
{
loadBundle(baseName, locale, ClassLoader.getSystemClassLoader());
}
/**
* This creates a Resource bundle the name of the
* the resource bundle (e.g. "language.JX"). It then uses
* the specified locale to track down the
* appropriate translation file, and teh specified class loader
* to retrieve the file.
* @param baseName the name of the translation file to look up.
* this name is extended using the standard locality rules
* to try to find localised files (e.g. "language.JX" becomes
* language/JX_fr_CA in french-speaking canada).
* @param locale a specific locale to use in place of the default
* system locale.
* @param loader a custom class loader (such as CBClassLoader) used to
* retrieve the translation file.
*/
public CBResourceBundle(String baseName, Locale locale, ClassLoader loader)
{
loadBundle(baseName, locale, loader);
}
/**
* This method searches through all the valid permutations of the base
* bundle name (modified for locale - e.g. JX_fr_CA.properties, JX_fr.properties,
* and JX.properties...). <p>
* If successful, it loads the data (the translation strings) into a local
* data store (Hashtable).
*
* @param baseName the name of the translation file to look up.
* this name is extended using the standard locality rules
* to try to find localised files (e.g. "language.JX" becomes
* language/JX_fr_CA in french-speaking canada).
* @param locale a specific locale to use in place of the default
* system locale.
* @param loader a custom class loader (such as CBClassLoader) used to
* retrieve the translation file.
*/
protected void loadBundle(String baseName, Locale locale, ClassLoader loader)
{
Vector names = getBundleNames(baseName, locale);
for (int i=names.size()-1; i>=0; i--)
{
URL url = loader.getResource(names.get(i).toString()); // XXX why getResource, not findResource???
if (loadData(url) == true) {
return; // once a single file has been loaded, we're done.
}
}
// couldn't succesfully load anything...
CBUtility.log("unable to load resource bundle '" + baseName + "'");
return;
}
/**
* Calculate the bundles along the search path from the base bundle to the
* bundle specified by baseName and locale.
* @param baseName the base bundle name
* @param locale the locale
* @param names the vector used to return the names of the bundles along
* the search path.
*
*/
protected static Vector getBundleNames(String baseName, Locale locale)
{
final Vector result = new Vector(8);
final String language = locale.getLanguage();
final int languageLength = language.length();
final String country = locale.getCountry();
final int countryLength = country.length();
final String variant = locale.getVariant();
final int variantLength = variant.length();
if (baseName.toLowerCase().endsWith(".properties"))
{
baseName = baseName.substring(baseName.length()-11);
}
baseName = baseName.replace('.', '/'); // note forward slash used, rather than File.separator, for jar access etc.
final StringBuffer temp = new StringBuffer(baseName);
result.addElement(temp.toString() + ".properties");
result.addElement(temp.toString());
if (languageLength + countryLength + variantLength == 0)
{
return result; //The locale is "", "", "".
}
temp.append('_');
temp.append(language);
result.addElement(temp.toString() + ".properties");
result.addElement(temp.toString());
if (countryLength + variantLength == 0)
{
return result;
}
temp.append('_');
temp.append(country);
result.addElement(temp.toString() + ".properties");
result.addElement(temp.toString());
if (variantLength == 0)
{
return result;
}
temp.append('_');
temp.append(variant);
result.addElement(temp.toString() + ".properties");
result.addElement(temp.toString());
return result;
}
/**
* This loads the data from a translation file, checking on the way
* what file format it is in. (UTF-8, 16bit unicode, or local encoding).
* @param url the URL to read the data InputStream from.
* @return whether the load data operation was successfull, or whether
* it was interupted (for whatever reason; no file, error reading
* file, bad encoding, yadda yadda yadda).
*/
protected boolean loadData(URL url)
{
if (url == null) {
return false; // can't read from a null url!
}
CBUtility.log("Resource Bundle Reading data from " + ((url==null)?"null url":url.toString()), 6);
try
{
/*
* First, slurp all the data from the input stream into a byte array.
*/
byte[] data = CBUtility.readStream(url.openStream());
/*
* Convert the byte array to a String using cunning auto-detecting
* encoding methods.
*/
String text = CBUtility.readI18NByteArray(data);
/*
* Load up the translations hashtable with the parsed data found
* in the string...
*/
return parseData(text);
}
catch (Exception e)
{
CBUtility.log("Unable to read data from url: " + ((url==null)?"(null url)":url.toString()) + " \n -> exception was " + e, 7);
return false;
}
}
/**
* parses the byte array as per a normal resource file
* (i.e. looking for key/data pairs seperated by an
* unescaped '=' sign) after first converting the byte
* array into a String, using whichever language encoding
* (unicode16, utf8, locale-specific) seems appropriate.
*/
protected boolean parseData(String text)
{
int startSize = this.translations.size();
int start =0, end = 0;
while ((end = text.indexOf('\n',start)) != -1)
{
String line = text.substring(start, end);
line = line.trim();
if (line.length() != 0 && line.charAt(0) != '#') // ignore blank lines and commented lines.
{
try
{
int equalPos = 0;
do // skip through all escaped equals characters until we find a non-escaped one.
{
equalPos = line.indexOf('=', equalPos + 1);
}
while (line.charAt(equalPos-1) == '\\');
String key = unescape(line.substring(0,equalPos)).trim();
String trans = line.substring(equalPos+1).trim();
this.translations.put(key, trans);
}
catch (Exception e)
{
CBUtility.log("Exception parsing data line '" + line + "' -> " + e, 8);
} // prob. array ex. - ignore this line.
}
start = end+1;
}
// check if we added any new translations - if we did, then this was
// at least partially successfull.
boolean success = (startSize < this.translations.size());
if (success == false) {
CBUtility.log("ParseData unsuccessfull - no new data found", 8);
}
return success;
}
/**
* Removes all escapes ('\?' -> '?') from a string.
* -> Not particularly efficient, but o.k. for short strings.
*/
protected String unescape(String escapeMe)
{
int pos = 0;
while ((pos = escapeMe.indexOf('\\', pos)) >= 0) {
escapeMe = escapeMe.substring(0, pos) + escapeMe.substring(pos+1);
}
return escapeMe;
}
/**
* returns the translation keys.
* @return an Enumeration of all the known keys (usually translatable
* strings).
*/
public Enumeration keys() { return this.translations.keys(); }
/**
* returns the translation keys. Synonym for 'keys()', kept
* for compatibility with ResourceBundle.
* @return an Enumeration of all the known keys (usually translatable
* strings).
*/
public Enumeration getKeys() { return this.translations.keys(); }
/**
* Returns the object corresponding to a given key.
* @param key the original text to translate/look up
* @return the corresponding translation/object
*/
public Object get(Object key) { return this.translations.get(key); }
/**
* Returns the object corresponding to a given key. kept
* for compatibility with ResourceBundle.
* @param key the original text to translate/look up
* @return the corresponding translation/object
*/
public Object getObject(Object key) { return this.translations.get(key); }
/**
* Convenience class returning a particular object
* as a String. If the object <i>was</i> a String
* already it is passed back unchanged, otherwise
* 'toString()' is called on the object before returning.
* This class never throws a ClassCastException.
* @param key the original text to translate/look up
* @return the corresponding translation/object as a String
*/
public String getString(String key)
{
if (key == null) {
return "";
}
Object o = this.translations.get(key);
if (o == null) {
return "";
}
return (o instanceof String)?(String)o:o.toString();
}
}