/*
* $Id$
* This file is a part of the Arakhne Foundation Classes, http://www.arakhne.org/afc
*
* Copyright (c) 2000-2012 Stephane GALLAND.
* Copyright (c) 2005-10, Multiagent Team, Laboratoire Systemes et Transports,
* Universite de Technologie de Belfort-Montbeliard.
* Copyright (c) 2013-2016 The original authors, and other authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.arakhne.afc.vmutil.locale;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.List;
import java.util.Map.Entry;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.eclipse.xtext.xbase.lib.Inline;
import org.eclipse.xtext.xbase.lib.Pure;
import org.arakhne.afc.vmutil.Caller;
import org.arakhne.afc.vmutil.ClassLoaderFinder;
/**
* This utility class permits a easier use of localized strings.
* <code>Locale</code> provides a means to retreive
* messages in the default language. Use this to construct messages
* displayed for end users.
*
* <p><code>Locale</code> takes a string from a properties resource,
* then inserts the parameter strings into the extracted strings
* at the appropriate places.
* The pattern matching is proceeded with {@link LocaleMessageFormat}
* formatter. Note that <code>''</code> may represent a single quote
* in strings (see {@link LocaleMessageFormat} for details).
*
* @author $Author: sgalland$
* @author $Author: lamotte$
* @version $FullVersion$
* @mavengroupid $GroupId$
* @mavenartifactid $ArtifactId$
* @since 6.4
*/
public final class Locale {
private static final int BUFFER_SIZE = 2048;
private Locale() {
//
}
private static Class<?> detectResourceClass(Class<?> resource) {
if (resource == null) {
// Parameter value:
// 0: is always Locale.class (ie. this PRIVATE function)
// 1: is always Locale.class (ie. the caller of this PRIVATE function)
// 2: is the third top of the trace stack ie, the first caller outside Locale.class
return Caller.getCallerClass(2);
}
return resource;
}
/**
* Replies the text that corresponds to the specified resource.
*
* <p>The <code>resourcePath</code> argument should be a fully
* qualified class name. However, for compatibility with earlier
* versions, Sun's Java SE Runtime Environments do not verify this,
* and so it is possible to access <code>PropertyResourceBundle</code>s
* by specifying a path name (using "/") instead of a fully
* qualified class name (using ".").
*
* @param resourcePath is the name (path) of the resource file, a fully qualified class name
* @param key is the name of the resource into the specified file
* @param defaultValue is the default value to replies if the resource does not contain the specified key.
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
@Inline(value = "Locale.getStringWithDefaultFrom(ClassLoaderFinder.findClassLoader(), ($1), ($2), ($3), ($4))",
imported = {Locale.class, ClassLoaderFinder.class})
public static String getStringWithDefaultFrom(String resourcePath, String key, String defaultValue, Object... params) {
// This method try to use the plugin manager class loader
// if it exists, otherwhise, it use the default class loader
return getStringWithDefaultFrom(
ClassLoaderFinder.findClassLoader(),
resourcePath, key, defaultValue, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* <p>The <code>resourcePath</code> argument should be a fully
* qualified class name. However, for compatibility with earlier
* versions, Sun's Java SE Runtime Environments do not verify this,
* and so it is possible to access <code>PropertyResourceBundle</code>s
* by specifying a path name (using "/") instead of a fully
* qualified class name (using ".").
*
* @param classLoader is the class loader to use, a fully qualified class name
* @param resourcePath is the name (path) of the resource file
* @param key is the name of the resource into the specified file
* @param defaultValue is the default value to replies if the resource does not contain the specified key.
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getStringWithDefaultFrom(ClassLoader classLoader, String resourcePath, String key,
String defaultValue, Object... params) {
if (resourcePath == null) {
return defaultValue;
}
// Get the resource file
ResourceBundle resource = null;
try {
resource = ResourceBundle.getBundle(resourcePath,
java.util.Locale.getDefault(),
classLoader);
} catch (MissingResourceException exep) {
return defaultValue;
}
// get the resource string
String result;
try {
result = resource.getString(key);
} catch (Exception e) {
return defaultValue;
}
// replace the \n and \r by a real new line character
result = result.replaceAll("[\\n\\r]", "\n"); //$NON-NLS-1$ //$NON-NLS-2$
result = result.replaceAll("\\t", "\t"); //$NON-NLS-1$ //$NON-NLS-2$
// replace the parameter values
assert params != null;
return LocaleMessageFormat.format(result, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* <p>The <code>resourcePath</code> argument should be a fully
* qualified class name. However, for compatibility with earlier
* versions, Sun's Java SE Runtime Environments do not verify this,
* and so it is possible to access <code>PropertyResourceBundle</code>s
* by specifying a path name (using "/") instead of a fully
* qualified class name (using ".").
*
* @param resourcePath is the name (path) of the resource file, a fully qualified class name
* @param key is the name of the resource into the specified file
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
@Inline(value = "Locale.getStringWithDefaultFrom(($1), ($2), ($3), ($4))",
imported = {Locale.class})
public static String getStringFrom(String resourcePath, String key, Object... params) {
return getStringWithDefaultFrom(resourcePath, key, key, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* <p>The <code>resourcePath</code> argument should be a fully
* qualified class name. However, for compatibility with earlier
* versions, Sun's Java SE Runtime Environments do not verify this,
* and so it is possible to access <code>PropertyResourceBundle</code>s
* by specifying a path name (using "/") instead of a fully
* qualified class name (using ".").
*
* @param classLoader is the classLoader to use.
* @param resourcePath is the name (path) of the resource file, a fully qualified class name
* @param key is the name of the resource into the specified file
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
@Inline(value = "Locale.getStringWithDefaultFrom(($1), ($2), ($3), ($3), ($4))",
imported = {Locale.class})
public static String getStringFrom(ClassLoader classLoader, String resourcePath, String key, Object... params) {
return getStringWithDefaultFrom(classLoader, resourcePath, key, key, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* @param resource is the name of the resource file
* @param key is the name of the resource into the specified file
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getString(Class<?> resource, String key, Object... params) {
return getString(ClassLoaderFinder.findClassLoader(), detectResourceClass(resource), key, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* @param classLoader is the class loader to use.
* @param resource is the name of the resource file
* @param key is the name of the resource into the specified file
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getString(ClassLoader classLoader, Class<?> resource, String key, Object... params) {
Class<?> res = detectResourceClass(resource);
if (res == null) {
return key;
}
String val = getStringWithDefaultFrom(classLoader, res.getCanonicalName(), key, null, params);
if (val == null && classLoader != resource.getClassLoader()) {
val = getStringWithDefaultFrom(classLoader, res.getCanonicalName(), key, null, params);
}
while ((res != null) && (val == null)) {
res = res.getSuperclass();
if (res != null) {
val = getStringWithDefaultFrom(classLoader, res.getCanonicalName(), key, null, params);
}
}
if (val == null) {
return key;
}
return val;
}
/**
* Replies the text that corresponds to the specified resource.
*
* <p>This function assumes the classname of the caller as the
* resource provider.
*
* @param key is the name of the resource into the specified file
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
public static String getString(String key, Object... params) {
final Class<?> resource = detectResourceClass(null);
return getString(ClassLoaderFinder.findClassLoader(), resource, key, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* <p>This function assumes the classname of the caller as the
* resource provider.
*
* @param classLoader is the classLoader to use.
* @param key is the name of the resource into the specified file
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getString(ClassLoader classLoader, String key, Object... params) {
return getString(classLoader, detectResourceClass(null), key, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* @param resource is the name of the resource file
* @param key is the name of the resource into the specified file
* @param defaultValue is the default value to replies if the resource does not contain the specified key.
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getStringWithDefault(Class<?> resource, String key, String defaultValue, Object... params) {
return getStringWithDefault(ClassLoaderFinder.findClassLoader(), detectResourceClass(resource),
key, defaultValue, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* @param classLoader is the class loader to use.
* @param resource is the name of the resource file
* @param key is the name of the resource into the specified file
* @param defaultValue is the default value to replies if the resource does not contain the specified key.
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getStringWithDefault(ClassLoader classLoader, Class<?> resource, String key,
String defaultValue, Object... params) {
Class<?> res = detectResourceClass(resource);
if (res == null) {
return defaultValue;
}
String val = getStringWithDefaultFrom(classLoader, res.getCanonicalName(), key, null, params);
if (val == null && classLoader != resource.getClassLoader()) {
val = getStringWithDefaultFrom(classLoader, res.getCanonicalName(), key, null, params);
}
while ((res != null) && (val == null)) {
res = res.getSuperclass();
if (res != null) {
val = getStringWithDefaultFrom(classLoader, res.getCanonicalName(), key, null, params);
}
}
if (val == null) {
return defaultValue;
}
return val;
}
/**
* Replies the text that corresponds to the specified resource.
*
* @param key is the name of the resource into the specified file
* @param defaultValue is the default value to replies if the resource does not contain the specified key.
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getStringWithDefault(String key, String defaultValue, Object... params) {
return getStringWithDefault(ClassLoaderFinder.findClassLoader(), detectResourceClass(null), key, defaultValue, params);
}
/**
* Replies the text that corresponds to the specified resource.
*
* @param classLoader is the class loader to use.
* @param key is the name of the resource into the specified file
* @param defaultValue is the default value to replies if the resource does not contain the specified key.
* @param params is the the list of parameters which will
* replaces the <code>#1</code>, <code>#2</code>... into the string.
* @return the text that corresponds to the specified resource
*/
@Pure
public static String getStringWithDefault(ClassLoader classLoader, String key, String defaultValue, Object... params) {
return getStringWithDefault(classLoader, detectResourceClass(null), key, defaultValue, params);
}
/** Decode the specified array of bytes according to
* a charset selection. This function tries
* to decode a string from the given byte array
* with the following charsets (in preferred order):
* <ul>
* <li>the current charset returned by {@link Charset#defaultCharset()},</li>
* <li>OEM United States: IBM437,</li>
* <li>West European: ISO-8859-1,</li>
* <li>one of the chars returned by {@link Charset#availableCharsets()}.</li>
* </ul>
*
* <p>The IBM437 charset was added to support several specific files (Dbase files)
* generated from a GIS.
*
* @param bytes is the array of bytes to decode.
* @return the decoded string with the appropriate charset set.
*/
@Pure
public static String decodeString(byte[] bytes) {
final Charset defaultCharset = Charset.defaultCharset();
final Charset westEuropean = Charset.forName("ISO-8859-1"); //$NON-NLS-1$
final Charset utf = Charset.forName("UTF-8"); //$NON-NLS-1$
final String refBuffer = new String(bytes);
CharBuffer buffer = decodeString(bytes, defaultCharset, refBuffer.length());
if ((buffer == null) && (!defaultCharset.equals(westEuropean))) {
buffer = decodeString(bytes, westEuropean, refBuffer.length());
}
if ((buffer == null) && (!defaultCharset.equals(utf))) {
buffer = decodeString(bytes, utf, refBuffer.length());
}
if (buffer == null) {
// Decode until one of the available charset replied a value
for (final Charset charset : Charset.availableCharsets().values()) {
buffer = decodeString(bytes, charset, refBuffer.length());
if (buffer != null) {
break;
}
}
}
// Use the default encoding
if (buffer == null) {
return refBuffer;
}
return buffer.toString();
}
/** Decode the specified array of bytes with the specified charset.
*
* @param bytes is the array of bytes to decode.
* @param charset is the charset to use for decoding
* @param referenceLength is the length of the attempted result. If negative, this parameter is ignored.
* @return the decoded string with the appropriate charset set,
* or <code>null</code> if the specified charset cannot be
* used to decode all the characters inside the byte array.
*/
private static CharBuffer decodeString(byte[] bytes, Charset charset, int referenceLength) {
try {
final Charset autodetectedCharset;
final CharsetDecoder decoder = charset.newDecoder();
final CharBuffer buffer = decoder.decode(ByteBuffer.wrap(bytes));
if ((decoder.isAutoDetecting())
&& (decoder.isCharsetDetected())) {
autodetectedCharset = decoder.detectedCharset();
if (charset.contains(autodetectedCharset)) {
buffer.position(0);
if ((referenceLength >= 0) && (buffer.remaining() == referenceLength)) {
return buffer;
}
return null;
}
}
// Apply a proprietaty detection
buffer.position(0);
char c;
int type;
while (buffer.hasRemaining()) {
c = buffer.get();
type = Character.getType(c);
switch (type) {
case Character.UNASSIGNED:
case Character.CONTROL:
case Character.FORMAT:
case Character.PRIVATE_USE:
case Character.SURROGATE:
// Character not supported?
return null;
default:
}
}
buffer.position(0);
if ((referenceLength >= 0) && (buffer.remaining() == referenceLength)) {
return buffer;
}
} catch (CharacterCodingException e) {
//
}
return null;
}
/** Decode the bytes from the specified input stream
* according to a charset selection. This function tries
* to decode a string from the given byte array
* with the following charsets (in preferred order):
* <ul>
* <li>the current charset returned by {@link Charset#defaultCharset()},</li>
* <li>OEM United States: IBM437,</li>
* <li>West European: ISO-8859-1,</li>
* <li>one of the charst returned by {@link Charset#availableCharsets()}.</li>
* </ul>
*
* <p>The IBM437 charset was added to support several specific files (Dbase files)
* generated from a GIS.
*
* @param stream is the stream to decode.
* @return the decoded string with the appropriate charset set.
* @throws IOException when the stream cannot be read.
*/
public static String decodeString(InputStream stream) throws IOException {
byte[] completeBuffer = new byte[0];
final byte[] buffer = new byte[BUFFER_SIZE];
int read;
while ((read = stream.read(buffer)) > 0) {
final byte[] tmp = new byte[completeBuffer.length + read];
System.arraycopy(completeBuffer, 0, tmp, 0, completeBuffer.length);
System.arraycopy(buffer, 0, tmp, completeBuffer.length, read);
completeBuffer = tmp;
}
return decodeString(completeBuffer);
}
/** Decode the bytes from the specified input stream
* according to a charset selection. This function tries
* to decode a string from the given byte array
* with the following charsets (in preferred order):
* <ul>
* <li>the current charset returned by {@link Charset#defaultCharset()},</li>
* <li>OEM United States: IBM437,</li>
* <li>West European: ISO-8859-1,</li>
* <li>one of the charst returned by {@link Charset#availableCharsets()}.</li>
* </ul>
*
* <p>The IBM437 charset was added to support several specific files (Dbase files)
* generated from a GIS.
*
* <p>This function read the input stream line by line.
*
* @param stream is the stream to decode.
* @param lineArray is the array of lines to fill
* @return <code>true</code> is the decoding was successful,
* otherwhise <code>false</code>
* @throws IOException when the stream cannot be read.
*/
public static boolean decodeString(InputStream stream, List<String> lineArray) throws IOException {
// Read the complete file
byte[] completeBuffer = new byte[0];
final byte[] buffer = new byte[BUFFER_SIZE];
int read;
while ((read = stream.read(buffer)) > 0) {
final byte[] tmp = new byte[completeBuffer.length + read];
System.arraycopy(completeBuffer, 0, tmp, 0, completeBuffer.length);
System.arraycopy(buffer, 0, tmp, completeBuffer.length, read);
completeBuffer = tmp;
}
// Get the two default charsets
//Charset oem_us = Charset.forName("IBM437");
final Charset westEuropean = Charset.forName("ISO-8859-1"); //$NON-NLS-1$
final Charset defaultCharset = Charset.defaultCharset();
// Decode with the current charset
boolean ok = decodeString(new ByteArrayInputStream(completeBuffer), lineArray, defaultCharset);
// Decode with the default oem US charset
/*if ((!ok)&&(!default_charset.equals(oem_us))) {
ok = decodeString(new ByteArrayInputStream(complete_buffer),lineArray,oem_us);
}*/
// Decode with the default west european charset
if ((!ok) && (!defaultCharset.equals(westEuropean))) {
ok = decodeString(new ByteArrayInputStream(completeBuffer), lineArray, westEuropean);
}
// Decode until one of the available charset replied a value
if (!ok) {
for (final Entry<String, Charset> charset : Charset.availableCharsets().entrySet()) {
if (decodeString(new ByteArrayInputStream(completeBuffer), lineArray, charset.getValue())) {
return true;
}
}
}
return ok;
}
/** Decode the bytes from the specified input stream
* according to a charset selection. This function tries
* to decode a string from the given byte array
* with the following charsets (in preferred order).
*
* <p>This function read the input stream line by line.
*
* @param stream is the stream to decode.
* @param lineArray is the array of lines to fill.
* @param charset is the charset to use.
* @return <code>true</code> is the decoding was successful,
* otherwhise <code>false</code>
* @throws IOException when the stream cannot be read.
*/
private static boolean decodeString(InputStream stream, List<String> lineArray, Charset charset) throws IOException {
try {
final BufferedReader breader = new BufferedReader(
new InputStreamReader(stream, charset.newDecoder()));
lineArray.clear();
String line;
while ((line = breader.readLine()) != null) {
lineArray.add(line);
}
return true;
} catch (CharacterCodingException exception) {
//
}
return false;
}
}