13.6.13

Abordando problemas de encoding, en Java

Posiblemente, los problemas derivados de un mal uso o mala comprensión del encoding, sea uno de los principales dolores de cabeza para muchos desarrolladores. Es fácil determinar que cuando en una web estamos viendo «camión» en vez de «camión», es que pasa algo con el encoding. Pero ¿qué está pasando exactamente? En realidad, no es algo tan complicado una vez se entiende todo el asunto correctamente. Para ello, recomiendo la lectura del artículo What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text. Es más, me atrevo a decir que es de lectura obligada para todo aquel que no haya sabido determinar inmediatamente, por qué podríamos ver «camión» en vez de «camión» (y no, no vale decir simplemente «algo del encoding»).

Para los que no puedan leer con fluidez la lengua de Shakespeare, resumiré los puntos más importantes:

  1. Para un ordenador, todo son números. No, miento, son ceros y unos. Bits. El que parezca que se almacenen letras y símbolos, es porque se utiliza una relación biunívoca (uno a uno) entre caracteres y bits.
  2. Un charset o juego de caracteres, es un conjunto de caracteres representables, con un código numérico asociado a cada uno. Un encoding o codificación, es la forma de representar en bytes, el código numérico del carácter.
  3. Unicode no es un encoding, es un charset. Uno muy interesante, ya que incluye todos los posibles caracteres de todos los idiomas del mundo. UTF-8 y UTF-16 son encodings de Unicode. Esta distinción es irrelevante en codificaciones donde no hay más de 256 caracteres, como ASCII o ISO-8859-1.
  4. La codificación de los primeros 127 caracteres de UTF-8, la familia ISO-8859, Windows-1252, y muchos otros, es igual que la ASCII. ASCII no incluye vocales acentuadas o la eñe. Es por eso que sólo aparecen problemas de encoding cuando usamos estos caracteres.
  5. En UTF-8, para caracteres con código mayor que 127 (hex 7F), se emplea más de un byte (hasta un máximo de 4). No todas las secuencias de bytes son válidas. Cuando la aplicación encuentra un byte conflictivo, puede mostrar el símbolo de cierre de interrogación «?» o el carácter de sustitución Unicode «�» (U+FFFD)

Para los lenguajes que usan el alfabeto latino, podemos encontrar varios sistemas de codificación diferentes. En Windows, por ejemplo, se utiliza por defecto la codificación Windows-1252 (o CP-1252). En Linux, se utiliza generalmente UTF-8. En algunos servidores no Windows podemos encontrar ISO-8859-1 ó ISO-8859-15. Cuando un texto se muestra de forma extraña, el problema siempre es el mismo: el texto está almacenado con una codificación determinada, pero la aplicación que lo muestra lo está interpretando con otra codificación.

Veamos un ejemplo. La cadena "año", se representa en UTF-8 como 61 C3 B1 6F. Es importante recordar que la 'ñ' se representa con dos bytes: C3 B1 ¿Qué ocurre si una aplicación interpreta esta secuencia de bytes como un texto codificado en CP-1252? En esta codificación, el byte C3 corresponde al caracter 'Ã' y el byte B1 al caracter '±', por lo que esa aplicación mostrará "año".

Vamos con el ejemplo contrario. La cadena "año" se representa en CP-1252 como 61 F1 6F. Si una aplicación intenta interpretar esta secuencia de bytes como si fuera UTF-8, se encontrará con que el byte F1 (11110001) es el primer byte de una secuencia de 4, pero que el siguiente byte debería estar entre 80 y BF (cumplir el patrón 10xxxxxx) y no es así. Al ser F1 un carácter «conflictivo», lo escapará con un '�', continuando con la intrepretación. De esta forma, se mostrará "a�o".

El problema puede complicarse si al cargar un dato se comete un error de codificación, y al mostrarlo se comete otro, teniendo entonces un «doble error». Imaginad, por ejemplo, una cadena "año" codificada en UTF-8, pero interpretada como ISO-8859-15. Como desde el punto de vista de una máquina, la cadena "año" es perfectamente válida, no saltaría ningún error. Si más adelante, a la hora de enviar el dato a otro sistema (por ejemplo, desde una aplicación web al navegador del usuario), se codifica como UTF-8 pero se interpreta en destino como CP-1252, tendríamos un «doble error», y veríamos "año": El motivo es que 'Ã' se representa en UTF-8 como c3 83, y '±' como c2 b1. En CP-1252, el byte c3 corresponde a 'Ã', el 83 a 'ƒ', el c2 a 'Â' y el b1 a '±'.

Con esto ya tenemos unas de reglas de andar por casa, para averiguar lo que está pasando cuando vemos textos raros:

  1. Si vemos caracteres � en vez de vocales acentuadas o eñes, la aplicación está interpretando como UTF-8 un texto que en realidad está almacenado como CP-1252 o ISO-8859.
  2. Si vemos que las eñes o vocales acentuadas son sustituidas por dos caracteres "extraños", la aplicación está interpretando como CP-1252 o ISO-8859 un texto que en realidad está codificado como UTF-8.
  3. En general, si vemos que las eñes o vocales acentuadas son sustituidas por 2n caracteres "extraños", se está cometiendo el error anterior n veces a lo largo del ciclo de vida del texto.

Las codificaciones CP-1252, ISO-8859-1 e ISO-8859-15, codifican de igual forma las vocales acentuadas y eñes. Si queremos hilar más fino y diferenciarlas, tenemos que toparnos con el símbolo del euro: '€'. En CP-1252 se codifica como 80, que en ISO-8859-1 e ISO-8859-15 corresponden a un caracter de control no imprimible. En ISO-8859-15 se codifica como A4, que en CP-1252 y en ISO-8859-1 corresponde al caracter '¤'. Y en ISO-8859-1, sencillamente no existe dicho símbolo (y por eso, es una codificación que debería abandonarse cuanto antes). Para completar, en UTF-8, el € se representa como E2 82 AC. Es decir, con 3 bytes, por lo que habrá que tenerlo en cuenta al aplicar las reglas mencionadas.

En una aplicación mínimamente compleja, desarrollada en Java, el error de codificación puede tener múltiples orígenes. Así que vamos a ir revisando poco a poco qué codificaciones se usan en cada punto, y cómo se pueden cambiar.

Como todo buen programador en Java debe saber, la plataforma utiliza internamente la codificación UTF-16 para almacenar en memoria las cadenas de texto. En realidad, a menos que necesitemos meternos en las tripas de bajo nivel de una JVM, este dato es una mera curiosidad, ya que a todos los efectos, una String es como un array de caracteres, es decir, de char. Salvo algunos métodos concretos, se opera a nivel de caracter. Así el resultado de


"1€ al año".length()

siempre será 9, sin importar la codificación, puesto que lo que nos devuelve es el número de caracteres, y no el número de bytes. Por el contrario, el número de bytes varía con la codificación, siendo 9 en ISO-8859-15, 12 en UTF-8 (el € son 3 bytes) y 18 en UTF-16 (2 bytes por caracter).

Siguiente paso: los fuentes. Un fichero .java no deja de ser un simple fichero de texto plano. Es el compilador el que leerá este fichero y generará el .class correspondiente. ¿Qué codificación usa el compilador? Pues por defecto utiliza la misma que la plataforma en la que se esté ejecutando. Esto es, si compilamos nuestros fuentes en Windows, se usará CP-1252. Si compilamos en Linux, se usará UTF-8. Es algo bastante razonable, puesto que la mayoría de editores que usemos, tendrán ese mismo comportamiento. Así, el siguiente ejemplo:


public class SillyExample {
    public static void main(String[] args) {
        System.out.println("El niño se gastó 1 € en chuches.");
    }
}

debería compilar y ejecutarse sin problemas, y mostrar en la consola lo mismo, sin importar la codificación de la plataforma:


El niño se gastó 1 € en chuches.

Process finished with exit code 0

Este comportamiento puede ser modificado, con la opción -encoding del compilador, que nos permite especificar la codificación de los fuentes. Obviamente, si la codificación de los ficheros .java no coincide con la que se use al compilar, ya tenemos un problema. Por lo general, un IDE en condiciones gestionará esto de forma transparente a nosotros, y si le decimos que use una codificación concreta, usará la misma al guardar los fuentes y en la opción del compilador.

Un primer paso para diagnósticar un problema de encoding con literales que se declaran en los fuentes, es averiguar la codificación de éstos. Para ello, lo más seguro es abrir un .java que sepamos que tiene algún carácter no ASCII, con un visor hexadecimal. Si el caracter está representado con dos bytes, el .java está en UTF-8. Si está representado con un solo byte, no.

No todas las cadenas que mostrará nuestra aplicación estarán en los .java. De hecho, eso no es una buena práctica. Lo habitual es tener los textos que se mostrarán al usuario en ficheros separados.

La forma habitual de mantener los textos que se mostrarán al usuario es mediante los ficheros .properties, que además tienen la ventaja de facilitar enormemente la internacionalización de la aplicación. Y aquí nos topamos con una limitación: la clase utilizada habitualmente para leer los ficheros .properties, ResourceBundle, interpreta siempre los ficheros como ISO-8859-1, no importa lo que hagamos. Esto puede suponer un inconveniente en algunos casos, ya que el juego de caracteres de esta codificación, no incluye el símbolo del euro. Afortunadamente, podemos especificar caracteres no incluidos en ISO-8859-1 utilizando el formato \uXXXX, donde XXXX es el código Unicode del caracter. Así, podríamos tener un .properties con la siguiente línea:


text=El niño se gastó 1 \u20AC en chuches.

Si queremos que nuestros .properties estén en otra codificación, como UTF-8, podríamos usar PropertyResourceBundle, ya que tiene un constructor público al que se le pasa un Reader, pero perderíamos las facilidades de ResourceBundle para la internacionalización. Otra alternativa es buscar funcionalidades similares en otras librerías o frameworks.

Ya que mencionamos la clase Reader, cuando leemos o escribimos un fichero con FileReader y FileWriter, se usa la codificación por defecto de la plataforma donde se está ejecutando. Esta misma codificación es la que se usa en toda conversión de caracteres a bytes y viceversa (con la excepción de los .properties leídos por ResorceBundle), como en el constructor String(byte[]) y el método byte[] getBytes() de String, o las clases InputStreamReader y OutputStreamWriter. En algunos casos, existirán variantes de los métodos en los que se les pueda especificar la codificación a usar de forma explícita, pero si no se hace, se usará la codificación por defecto de la plataforma.

Se puede modificar la codificación por defecto con la variable de sistema file.encoding, pero sólo podemos hacerlo en el arranque de la VM, con la opción -D (por ejemplo, -Dfile.encoding=UTF-8). Si lo hacemos en el código, usando System.setProperty("file.encoding", "UTF-8"), no funcionará. En cualquier caso, insisto en que esa configuración no afecta al comportamiento de ResourceBundle, que seguirá esperando los .properties en ISO-8859-1.

Así que aquí tenemos dos puntos más a revisar cuando encontramos problemas de encoding en una aplicación: la codificación de otros ficheros de texto además de los fuentes, el mecanismo utilizado para leerlos, y la codificación que está usando la JVM. Una forma sencilla de comprobar si los textos se están cargando con la codificación adecuada, es con el depurador de toda la vida. Si en un breakpoint evaluamos con un buen IDE el contenido de un String, podremos ver el número de caracteres, o mejor aun, el array de char interno. Si vemos los caracteres que deben donde deben, el problema no está en la lectura de ficheros u otros recursos.

Para ayudarnos en esta tarea, es muy útil la web FileFormat.Info, donde podemos consultar el código numérico de cualquier carácter Unicode, así como sus diferentes representaciones y más información útil. Echad un vistazo, por ejemplo, a nuestra querida «ñ».

Vamos ahora a entrar en el mundo de las aplicaciones web, que es donde suelen producirse estos errores (posiblemente porque Java tenga más presencia en la web que en el escritorio, y porque se introducen capas adicionales donde cometer errores). Una posibilidad es que el HTML se esté enviando por la red con una codificación, y el navegador la esté interpretando con otra.

Para determinar la codificación que debe emplear un navegador a la hora de mostrar el HTML de una response HTTP, se utiliza la cabecera HTTP Content-Type. Esta cabecera puede establecerse en la configuración del propio servidor (que variará dependiendo de qué utilicemos), en el código Java mediante los métodos setContentType(String) o setCharacterEncoding(String) del objeto ServletResponse (no son exactamente equivalentes, y hay peculiaridades que conviene consultar en la documentación), o mediante el atributo contentType de la directriz JSP page. Por ejemplo:


<%@ page contentType="text/html;charset=UTF-8"%>

Si esta cabecera no aparece, o no indica la codificación, el navegador busca una etiqueta meta presente en el HTML de la página, que a su vez puede ponerse como


<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>

o como


<meta charset='utf-8'>

Y si tampoco encuentra esta etiqueta, el navegador utilizará normalmente la codificación ISO-8859-1. Dado que esta codificación es anterior al euro, y no tiene el símbolo «€» en su juego de caracteres, si usamos esta codificación, nos veremos obligados a usar la entidad HTML &euro; para representarlo. De hecho, podemos limitarnos a usar únicamente caracteres ASCII, y emplear entidades HTML para los caracteres no ASCII, pero eso no soluciona todos los problemas, e introduce una incomodidad innecesaria a día de hoy.

Aunque se puede especificar la codificación del response sólo con los métodos de ServletResponse, si usamos JSPs debemos especificar la codificación también en la directiva page. Recordemos que una JSP se traduce internamente en un .java que luego se compila. La implementación concreta depende del servidor utilizado, pero puede ocurrir (y de hecho ocurre con Tomcat 6) que el encoding utilizado para generar el .java y compilarlo, dependa de esa directiva, de forma que si no aparece, se utilice una codificación diferente a la que deseamos (Tomcat 6 parece usar ISO-8859-1, independientemente de lo que especifiquemos en la propiedad file.encoding).

Otra fuente de error son los formularios. Cuando se hace submit de un formulario, el navegador utiliza en la request la misma codificación que haya usado para mostrar la página. Este comportamiento se puede modificar mediante el atributo accept-charset de la etiqueta form, indicando el encoding a usar (sólo el encoding, no todo el Content-Type). Sin embargo, no funciona en todos los navegadores, por lo que no es conveniente utilizarlo.

En el lado del servidor, la clase ServletRequest tiene un método getCharacterEncoding(), que debería devolvernos la codificación de la request. ¿Por qué digo «debería». Pues porque lo único que puede hacer es obtener dicho valor de la cabecera HTTP correspondiente. Los navegadores no suelen enviar esta información, por lo que en la mayoría de los casos, una llamada a este método devolverá un null. Es decir, no podemos confiar en dicho método para saber la codificación de la request. Así que debemos usar una de forma consistente en el response, y asumir que las request nos llegan con esa misma codificación.

Hay una gran excepción a esta regla (encoding de la request = encoding de la página del formulario), que sucede cuando usamos Ajax. Ajax usa por defecto UTF-8 en la request. Si no tenemos esto en cuenta, en una aplicación web en la que se use ISO-8859-15, por ejemplo, recibiremos una request UTF-8, que el servidor interpretará incorrectamente.

Una solución es especificar de forma explícita en nuestra aplicación, que la request viene codificada como UTF-8. Esto se puede hacer con el método setCharacterEncoding(String) de la clase ServletRequest. Este método simplemente establece con qué codificación se debe leer la request. Es decir, no realiza ninguna transformación de encoding. Si la request nos viene con una codificación, y establecemos una diferente con este método, los valores de los parámetros se leerán incorrectamente. Obviamente, hay que llamar a setCharacterEncoding(String) antes de leer algún parámetro de la request.

Una aplicación web que use una codificación en unas request, y otra diferente en otras, es muy mala idea. También es mala idea usar codificaciones diferentes para la request y el response. Lo mejor es usar la misma codificación de forma coherente en toda la aplicación. En JEE es muy sencillo establecer la codificación de request y response en un filtro (una clase que implemente Filter) y configurarlo en el web.xml. Como en las JSPs que usemos, debemos especificar en la directiva page esa misma codificación, podemos tener una JSP únicamente con esa directiva, e incluirla en todas las demás (o usar algún sistema de plantillas, para asegurarnos que todas incluyan la misma directiva).

Y ahora la gran pregunta: ¿qué codificación usar para toda nuestra aplicación? Mi elección personal es UTF-8, ya que al ser una codificación Unicode, tenemos acceso a todos los caracteres posibles. Además, puesto que Ajax utiliza esa misma codificación por defecto a la hora de hacer las request, evitamos tener que complicarnos con ese tema. El problema son los .properties, que si los vamos a leer con ResourceBundle, deben ser ISO-8859-1. Si nuestro IDE lo permite, podemos configurarlo para que todos los ficheros del proyecto estén en UTF-8 menos esos, o si usamos algún framework, buscar si incluye algún sistema alternativo que soporte UTF-8. También podemos optar por usar la clase PropertyResourceBundle, que nos permite usar un Reader, de forma que tengamos control de la codificación a usar, y si necesitamos multilenguaje, implementar nosotros mismos el comportamiento de ResourceBundle en ese caso (que básicamente es añadir al nombre el Locale utilizado, antes de la extensión, e ir eliminando fragmentos del mismo hasta encontrar el recurso).