viernes, 20 de septiembre de 2013

Archivos properties con codificación UTF-8

Java
En Java los archivos Properties que se cargan con la clase ResourceBundle utilizados comúnmente para realizar la internacionalización y la localización a varios idiomas en una aplicación Java por defecto usan la codificación de caracteres ISO-8859-1 (salvo que usemos un framework o una librería lo haga de otra forma). Los caracteres que no pertenezcan al ISO-8859-1 deben ser escapados, por ejemplo \u20AC para el símbolo del euro (€). Esto hace que si tenemos una aplicación que trabaja con caracteres en varios idiomas tengamos unos ficheros properties con un montón de caracteres de escape que impide su legibilidad al momento de escribirlos o nos obliga a usar el comando native2ascii lo que nos produce también archivos poco legibles.

La clase ResourceBundle permite cargar archivos properties según un Locale pero como digo los carga con la codificación ISO-8859-1 y esto es un problema para los locales como el chino donde casi todos los caracteres deben ser escapados. Si queremos tener archivos más legibles y sin necesidad de escapar los caracteres debemos extender la clase ResourceBundle.Control y redefinirla un poco para que cargue los properties en UTF-8. La implementación sería la siguiente:

package es.com.blogspot.elblogdepicodev.properties;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Locale;
import java.util.PropertyResourceBundle;
import java.util.ResourceBundle;
import java.util.ResourceBundle.Control;
public class EncodingControl extends Control {
private String encoding;
public EncodingControl(String encoding) {
this.encoding = encoding;
}
@Override
public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException,
IOException {
String bundleName = toBundleName(baseName, locale);
ResourceBundle bundle = null;
if (format.equals("java.class")) {
try {
Class<? extends ResourceBundle> bundleClass = (Class<? extends ResourceBundle>) loader.loadClass(bundleName);
// If the class isn't a ResourceBundle subclass, throw a
// ClassCastException.
if (ResourceBundle.class.isAssignableFrom(bundleClass)) {
bundle = bundleClass.newInstance();
} else {
throw new ClassCastException(bundleClass.getName() + " cannot be cast to ResourceBundle");
}
} catch (ClassNotFoundException e) {
}
} else if (format.equals("java.properties")) {
final String resourceName = toResourceName(bundleName, "properties");
final ClassLoader classLoader = loader;
final boolean reloadFlag = reload;
InputStream stream = null;
try {
stream = AccessController.doPrivileged(new PrivilegedExceptionAction<InputStream>() {
public InputStream run() throws IOException {
InputStream is = null;
if (reloadFlag) {
URL url = classLoader.getResource(resourceName);
if (url != null) {
URLConnection connection = url.openConnection();
if (connection != null) {
// Disable caches to get fresh data
// for
// reloading.
connection.setUseCaches(false);
is = connection.getInputStream();
}
}
} else {
is = classLoader.getResourceAsStream(resourceName);
}
return is;
}
});
} catch (PrivilegedActionException e) {
throw (IOException) e.getException();
}
if (stream != null) {
try {
bundle = new PropertyResourceBundle(new InputStreamReader(stream, encoding));
} finally {
stream.close();
}
}
} else {
throw new IllegalArgumentException("unknown format: " + format);
}
return bundle;
}
}
El código anterior es parte de la clase EncodingControl donde está redefinido el método newBundle. La magia está en la siguiente linea, donde al InputStreamReader se le indica la codificación de caracteres:

bundle = new PropertyResourceBundle(new InputStreamReader(stream, encoding));
Su uso para cargar los ResourceBundle sería:

ResourceBundle bundle = ResourceBundle.getBundle("utf8-2", new EncodingControl("UTF-8"));
view raw Main-1.java hosted with ❤ by GitHub
Y el programa completo en el que puede verse una carga de un archivo properties con codificación ISO-8859-1 y otro con codificación UTF-8:

package es.com.blogspot.elblogdepicodev.properties;
import java.util.ResourceBundle;
public class Main {
public static void main(String[] args) {
// Cargar un archivo properties en ISO-8859-1. Los caracteres del String obtenido de prueba se cargan correctamente.
{
ResourceBundle bundle = ResourceBundle.getBundle("default");
String s = bundle.getString("prueba");
System.out.println(s);
}
// Cargar un archivo properties UTF-8 con codificación ISO-8859-1. Los caracteres del String obtenido de prueba NO se cargan correctamente.
{
ResourceBundle bundle = ResourceBundle.getBundle("utf8-1");
String s = bundle.getString("prueba");
System.out.println(s);
}
// Cargar un archivo properties en UTF-8. Los caracteres del String obtenido de prueba se cargan correctamente.
{
ResourceBundle bundle = ResourceBundle.getBundle("utf8-2", new EncodingControl("UTF-8"));
String s = bundle.getString("prueba");
System.out.println(s);
}
}
}
view raw Main.java hosted with ❤ by GitHub
El resultado sería el siguiente:

El código fuente del ejemplo completo lo puedes encontrar en mi repositorio de GitHub.

Referencia:
http://stackoverflow.com/questions/4659929/how-to-use-utf-8-in-resource-properties-with-resourcebundle