Codificación de carácteres y Java

Hace unos días estuve peleándome(otra vez) con la codificación de carácteres en Java, se puede convertir en algo divertido el no saber exáctamente con qué codificaciones trabajas en cada momento cuando tienes varios orígenes de datos y recursos.

La cuestión es que al estar utilizando Grails que por defecto en la parte web utilizará UTF-8 para generar el html(se puede modificar, pero mejor usar UTF-8 para la internacionalización de carácteres), había que tratar de poner todas las codificaciones de carácteres en común (bases de datos, ficheros de texto, ficheros javascript…).

Primero, tenemos que asegurarnos de que nuestro IDE/editor guarde nuestros ficheros con formato UTF-8, igual que nuestra base de datos guarde el contenido con esta codificación, en MySQL:
CREATE DATABASE mydatabase CHARACTER SET utf8;
CREATE TABLE mytable (
...
) ENGINE=InnoDB CHARSET=utf8;

En PostgreSQL:
CREATE DATABASE mydatabase WITH ENCODING 'UTF8';
...

Por otro lado nos encontramos que Java utiliza nativamente UTF-16 (ver el api de la clase Charset) “The native character encoding of the Java programming language is UTF-16″, por lo que Groovy y Grails heredan esta codificación de carácteres.

Para seguir la uniformidad, la lectura/escritura de ficheros desde java la debemos hacer en UTF-8, la lectura la podemos hacer por medio de un InputStreamReader pasándole un FileInputStream y para la escritura por medio de un OutputStreamWriter con un FileOutputStream, y diciéndoles a ambos contructores con qué codificación, en nuestro caso UTF-8:
InputStreamReader isr = new InputStreamReader(myFileInputStream, "UTF-8")
...
OutputStreamWriter osw = new OutputStreamWriter(myFileOutputStream, "UTF-8")

Por último, y para rizar el rizo, nos encontramos que los ficheros Properties(y ResourceBundles) se cargan con codificación ISO-8859-1, por lo que en este caso nuestro editor debe escribir con esta codificación, y para leerlo programáticamente podemos hacer la transformación de la codificación a nivel de byte, esto sería:
Corregido por los comentarios de GreenEyed
ResourceBundle bundle = ResourceBundle.getBundle("com.danilat.foo")
String valueIso = bundle.getString(key)
String value = new String (valueIso.getBytes(), "ISO-8859-1");

Y en caso de que necesitemos forzar la conversión a UTF-8 de una cadena:
String valueUtf8= new String(string.getBytes("UTF-8"));

Por último, no debemos olvidarnos de añadir el tag meta content-type con el charset UTF-8 en el html, para que los navegadores sepan con qué codificación estamos mostrando nuestro contenido:
[meta http-equiv="content-type" content="text/html; charset=UTF-8"]

Como bonus, para Grails no es necesario ya que la codificación de carácteres es configurable en el Config.groovy, pero si quisieramos devolver contenido UTF-8 desde un Servlet o con un framework donde no fuera posible configurarlo, simplemente debemos acceder al objeto response y modificar la codificación de la respuesta :
response.setCharacterEncoding("UTF-8");

Si alguien quiere más explicaciones sobre codificaciones de carácteres y en particular sobre unicode, hay un artículo que para mi es de referencia de Joel Spolsky sobre Unicode, en inglés. También es interesante un post de Joaquín Cuenca en Programa con Google sobre el tema.

Por cierto, mucho cuidado con el Byte Order Mark (BOM) en ficheros UTF-8, que nos llevará a situaciones extrañas ya que Java lo reconoce como un espacio en blanco al principio de un fichero.

9 Responses to “Codificación de carácteres y Java”

  1. David Calavera Says:

    Hola Dani, una forma de evitar el problema con los properties y los ResourceBundle es usar las librerías de gettext, que manejan mucho mejor estos temas, te dejo una referencia:

    http://code.google.com/p/gettext-commons/

    por cierto, el autor del post de programa con google es “Joaquin Cuenca” no Joaquin Cuesta.

    Saludos

  2. Dani Says:

    Hola David,

    No conocía esta librería, le pegaré un repaso tranquilamente.

    Y lo bueno es que sé que es Joaquin Cuenca, pero parece que no soy dueño de mis dedos XD.

    Gracias por ambos apuntes.

  3. GreenEyed Says:

    Hola,
    String internamente guarda los caracteres en UTF-16 como dices, asi que no entiendo que diferencia hay, o crees que hay ;) , entre valueIso y valueUtf. Las dos cadenas _internamente_ son la misma, lo importante es a la hora de escribir un String como bytes o interpretar un String a partir de unos bytes, pero internamente te da igual como lo guarde.

    Es más, supuestamente si haces un getBytes() y luego creas un String con esos bytes con encodings differentes, podrias tener un error ya que estas generando la interpretacion en bytes como ISO-LATIN1 y la estas leyendo como si los bytes fuera UTF-8…
    Lo importante son los interfaces, y ahi el emisor y el receptor han de ponerse de acuerdo, como se guarde internamente la informacion deberia dar igual.

    Otro tema interesante y largamente comentado pero que aun “muerde” es el de los navegadores y su envio de parametros. Por cierto, el tag http-equiv=”content-type” y el response.setContentType son redundantes, aunque se suelen poner ambos por si acaso, el problema es que si te equivocas y no concuerdan… el resultado es imprevisible, por que cada navegador hace lo que le pasa por… :)

    Es un tema interesante.

  4. Dani Says:

    Hola GreenEyed,

    Las dos cadenas al final son la misma, pero en mi caso sí me interesa saber como lo guarda (o más bien forzar cómo lo guarda), prefiero hacerlo en la interfaz de entrada que en la de salida, ya que quién sabe si un día se modifica esa interfaz de entrada (por ejemplo para leer de un fichero de texto en UTF-8) mientras que la codificación de salida no cambiará.

    Sobre los posibles problemas, no acabo de entender a qué te refieres, la transformación es de Latin1 a UTF-16 en el getBytes y en el constructor del nuevo String es de UTF-16 a UTF-8.

    Sobre el tag y establecer el contentType, es redundante pero como dices, cualquiera se fía de los navegadores :) .

  5. GreenEyed Says:

    “me interesa saber como la guarda o forzar como la guarda” El problema de eso es que estas entendiendo el metodo al reves. Con String(bytes,codificacion), lo que le estas diciendo es la codificacion de los bytes que le estas pasando. Internamente String la seguira guardando en UTF-16, si o si. Codificacion se refiere a como estan codificados los bytes, no al String.

    Sobre el problema, pasa exactamente lo mismo con getBytes. Con getBytes(codificacion), el parametro codificacion no se refiere a como esta codificado internamente el String, que SIEMPRE esta codificado en UTF-16, se refiere a como quieres que codifique los bytes de salida. Asi que si obtienes los bytes codificados segun ISO y se los pasas a un constructor diciendole que estan codificados en UTF, estas pasandole argumentos incorrectos y te dara un error con algunos caracteres.

    Espero que asi quede mas claro :) .

    Prueba esto a ver si te aclara a lo que me refiero:

    System.err.println(”Prueba: ”
    + new String(”áéíóú@”.getBytes(”UTF-8″),”ISO-8859-1″))

    Verás que el resultado no es “áéíóú@”.

    S!

  6. Dani Says:

    Vale, ahora te he entendido :)

    Y sí, toda la razón, se modifica la codificación de los bytes y no la del String en sí que siempre será UTF-16 y como han cambiado los bytes cambia el contenido del String.

    La cuestión es que en alguna ocasión he tenido que forzar esa transformación de codificación, por no abstraerme de eso las interfaces.

    Todo lo que trata sobre estos cambios de codificaciones en el javadoc de la clase String es un poco confuso… al menos para mi:

    String(byte[] bytes, String charsetName)
    “Constructs a new String by decoding the specified array of bytes using the specified charset”
    ¿Un nuevo String decodificando el array de bytes usando el charset especificado?
    Se puede entender de las dos maneras…

    getBytes(String charsetName)
    “Encodes this String into a sequence of bytes using the named charset, storing the result into a new byte array.”
    ¿Codifica este String en una secuencia de bytes usando el charset nombrado, guardando el resultado en un array de bytes?
    Lo mismo…

    (Seguramente las traducciones dejan bastante que desear XD)

    Y por mi experiencia, quizás me equivoque haciéndolo de memoria pero estoy convencido de esto, el código de ejemplo viene a hacer:
    - El array de bytes lo leemos con codificación Latin1 para que el contenido del String “no se vea raro”, con getBytes(”ISO-8859-1″).
    - Y ese array lo decodificamos a UTF-8 en la creación del String para que en el destino se guarde “sin verse raro”.

    GreenEyed, a ver si llegamos a un acuerdo jejeje

  7. GreenEyed Says:

    Totalmente de acuerdo en que la documentacion es mas que confusa y el tema en si ya lo es. Yo lo tengo tan mirado por que antes pensaba que era como tu decias hasta que una vez tuve problemas y dandole vueltas y mirando mas cosas “encontre la luz” y todo cuadra.

    De todas formas, al leer (constructor) no se cambia la codificacion de los bytes, se cambia la forma de interpretarlos. Y al escribir (getBytes) no se cambia la cadena, se cambia la forma de transformar la cadena en bytes.

    El ejemplo, asi como está, hace:
    .- El String esta declarado como constante, crea un String y se codifica internamente en UTF-16
    .- El .getBytes(…UTF-8″) devuelve la ristra en bytes, segun se codifica en UTF-8, de la cadena.
    .- El new String(bytes,”ISO-8859-1″) interpreta la ristra de bytes como si codificaran unos caracteres en ISO, y los convierte en un String (internamente con UTF16).
    .- Como los caracteres que he puesto, a conciencia, no se representan igual en UTF-8 y LATIN1, pues se el constructor los interpreta mal.

    Haciendo una burda analogia, supongamos que UTF8 codifica las letras en numeros normales y LATIN1 en numeros romanos.

    “ABC”.getBytes(UTF-8) = 1,2,3
    “ABC”.getBytes(LATIN1) = I,II,III

    new String({1,2,3},UTF-8) = “ABC”
    new String({I,II,II},LATIN1) = “ABC”

    new String(”ABC”.getBytes(UTF-8),LATIN1) daria error por que el 1 en LATIN1 no es la A, el 2 no… etc.

    La transformacion que dices que hay que hacer a veces es cuando tienes que leer unos bytes que representan una cadena y estan codificados en un charset, llamemosle charsetX, distinto del de por defecto (usas new String(bytes,charsetX) o cuando tu tienes que esribir unos bytes en una codificacion diferente de la por defecto, que usas getBytes(charsetX) y escribes los bytes.

    S!

  8. Dani Says:

    Mmmm GreenEyed, creo que al final te voy a dar la razón :) .

    Tu último párrafo me hizo dudar y he comprobado que sí es como tú dices, corrijo el post.

    Muchas gracias por “ponerte tozudo” para hacerme entrar en razón ;)

  9. GreenEyed Says:

    Gracias a ti, estas discusiones quieras que no siempre ayudan a aclarar las cosas, y asi al que venga detras y lo lea le quedara mejor.
    Ademas, no podia dejar que otro “Dani” se quedara con una idea que no es… tenemos que mantener el nivel :P .

    Saludos, tocayo ;)

Leave a Reply