3.2.14

Tablas que no se muestran con FOP 0.20

FOP es la implementación de Apache del estándar XSL (también conocido como XSL-FO). Soporta varios formatos de salida, pero es habitual usarlo sólo para generar PDFs.

Las primeras versiones usables fueron las 0.20.4 y 0.20.5, allá por 2003. La siguiente release estable fue la 0.93 en 2007, y prácticamente se rehizo desde cero. Dado que las versiones 0.20 no implementaban correctamente el estándar XSL, a la hora de crear una plantilla para generar un PDF con un formato muy concreto, se tenía que recurrir a truquillos, trampas y (por qué no llamarlas así) ñapas. Con la llegada de la 0.93, al corregir muchos de los defectos de las versiones anteriores, las plantillas desarrolladas para las 0.20, no eran adecuadas para las modernas versiones, y los PDFs generados tenían una apariencia diferente a la deseada. La solución ideal, por supuesto, es adaptar dichas plantillas a la nueva versión. Pero claro, eso supone dedicar tiempo. Tiempo al que una empresa no ve rendimiento, ya que, después de todo, se va a modificar algo que ya funciona. Así que el estancarse en una versión concreta y antigua de FOP, se ha convertido en algunos sitios, en un dato del problema.

El problema aparece cuando hay que modificar o crear una plantilla que funcione con las 0.20, y la mayoría de información disponible en la red (al menos, la que aparece en primer lugar buscando con Google), se refiere a versiones más modernas. O también, cuando uno reaprovecha ese XSL que genera un PDF tan chulo con FOP 1.1, y al usarla con nuestro 0.20, el resultado es desolador.

Bien, recientemente sufrí uno de estos casos, cuando con una plantilla que funcionaba bien con la 0.94, al usarla con la 0.20.4, desaparecían tablas enteras. No se renderizaban en el PDF. Y el log no daba ninguna pista de por qué (no había errores).

El problema está en que la versión 0.20.4 (no sé si ocurre lo mismo con la 0.20.5), no soporta el autoajuste del ancho de las columnas. Es decir, hay que indicar de forma explícita el ancho de cada columna. Si no, la tabla no se mostrará. Veamos un ejemplo muy simple.


<fo:table>
  <fo:table-body>

    <fo:table-row>
      <fo:table-cell>
        (...)
      </fo:table-cell>
      <fo:table-cell>
        (...)
      </fo:table-cell>
    </fo:table-row>
    
    (...)
    
  </fo:table-body>
</fo:table>

He obviado algunas cosas, ya que sólo nos interesa la estructura de las etiquetas de tabla. Este fragmento de código podría formar parte de un XSL que funciona con versiones de FOP superiores o iguales a la 0.93. Sin embargo, con la 0.20.4, no mostrará nada de nada. Para que lo haga, debemos fijar el ancho de cada columna de la tabla:


<fo:table>

  <fo:table-column column-width="proportional-column-width(50)"/>
  <fo:table-column column-width="proportional-column-width(50)"/>

  <fo:table-body>

    <fo:table-row>
      <fo:table-cell>
        (...)
      </fo:table-cell>
      <fo:table-cell>
        (...)
      </fo:table-cell>
    </fo:table-row>
    
    (...)
    
  </fo:table-body>
</fo:table>

En este ejemplo, se establece que cada columna ocupe la mitad del ancho de la tabla (50% para cada una de las dos columnas). Fijaos en la función proportional-column-width. La versión 0.20.4 de FOP tampoco soporta el uso de porcentajes para el ancho (sí se pueden usar cm o pt), pero podemos saltar esta limitación con esta cómoda función (ojo, fijáos que se le pasa sólo un número; sin el símbolo «%»).

23.12.13

Pequeños «gotchas» de Date, Calendar y SimpleDateFormat en Java

El API de tiempo en Java es posiblemente el más vilipendiado, odiado e incomprendido de la plataforma. Pero es lo que hay, y a veces, la única opción que tenemos permitida (no siempre tenemos libertad para elegir librerías), así que voy a escribir un pequeño recetario/recordatorio de aquellas peculiaridades poco intuitivas, que aunque estén perfectamente documentadas, pueden inducir a error si no las tenemos en cuenta.

Primero voy a recordar lo más básico. Para la maquina virtual, la fecha y hora es un long, que indica el número de milisegundos transcurridos desde las 00:00 GMT del 1 de enero de 1970. Punto. No hay más. Las clases Date y Calendar son meros envoltorios con utilidades diversas.

Y ahora sí, vamos con los gotchas.

Meses en Calendar

En la clase Calendar, los meses empiezan desde cero. Es decir, enero es el mes 0, febrero es el mes 1, marzo el 2... y diciembre es el mes 11. La propia clase nos ofrece unas constantes con los nombres de los meses en inglés (JANUARY, FEBRUARY), pero es fácil olvidar este detalle y tener un disgusto. Sobre todo, cuando la clase SimpleDateFormat sí sigue el criterio intuitivo, y los meses empiezan con uno (enero es 1, febrero es 2, diciembre es 12).

HOUR vs. HOUR_OF_DAY

Calendar nos ofrece muchas constantes para referirnos a las distintas partes de la fecha y hora, pero cuidado con ellas. La constante HOUR se refiere a la hora, pero exclusivamente en formato AM/PM. Eso quiere decir que su valor está comprendido entre 1 y 12. Si hacemos


    calendar.set(Calendar.HOUR, 6);

estaremos estableciendo un valor diferente dependiendo de en qué momento del día se ejecute. Si es antes de las 12:00, estaremos indicando que la hora es 6 (cosa que seguramente es lo que queremos hacer), pero si ese código se ejecuta a las 12:00 o después, estaremos estableciendo 18 como hora. Si queremos usar el formato 24H (y especificar como hora un valor entre 0 y 23) debemos usar la constante HOUR_OF_DAY. Por ejemplo:


    calendar.set(Calendar.HOUR_OF_DAY, 6);

DATE no es lo que parece

Otra constante engañosa: DATE es equivalente a DAY_OF_MONTH y representa el día del mes. Haríamos bien en no usarla nunca, pero podríamos encontrarla en código ajeno, y conviene recordar qué significa.

Métodos iguales que no hacen lo mismo

Tanto Date como Calendar tienen un método getTime(), pero ojo, porque devuelven cosas diferentes. El getTime() de Date devuelve un long con el tiempo interno (los famosos milisegundos transcurridos desde las 00:00 GMT del 1 de enero de 1970), mientras que el getTime() de Calendar devuelve un objeto Date. Si queremos el long con los milisegundos, debemos usar getTimeInMillis()

Horas en SimpleDateFormat

Vamos con SimpleDateFormat y la forma de especificar un patrón. La letra «h» (minúscula) indica la hora en formato AM/PM, mientras que la «H» la indica en formato 24H. El siguiente formateador:


    SimpleDateFormat sdf = new SimpleDateFormat("hh:mm");

Nos devolverá 6:30, tanto si le pasamos un Date con la hora establecida a las 6:30 como a las 18:30. Si queremos usar el formato 24H (más habitual por estos lares), debemos usar:


    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");

SimpleDateFormat tiene otras letras para especificar la hora, pero creo que podemos ignorarlas, ya que nunca las he usado, y no creo que alguien lo haga por accidente («k» para 1-24 y «K» para 0-11).

No hay validaciones

Un comportamiento curioso de Calendar y SimpleDateFormat es que no hay limitación en el rango de valores. Es decir, podemos indicar fechas como el 31 de febrero, o el 40 de mayo. Como internamente la fecha en el fondo es un long con los milisegundos transcurridos desde la referencia 0, el valor incorrecto se convierte automáticamente en uno correcto. Así, el 31 de febrero correspondería al 3 de marzo (o el 2, si es un año bisiesto), y el 40 de mayo al 9 de junio (fecha hasta la que no debemos quitarnos el sayo). Eso quiere decir que no podemos usar estas clases para realizar algún tipo de validación en los rangos de valores de una entrada (como un formulario web, por ejemplo).

XML dateTime

Termino con algo que no es un «gotcha» sino una limitación. No hay forma de especificar un patrón para SimpleDateFormat que cumpla con el estándar XML, si queremos especificar la zona horaria. Si no especificamos la zona, basta con el siguiente patrón:


    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");

o incluso


    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");

si queremos llegar hasta el milisegundo. Pero si necesitamos especificar la zona horaria, tenemos un problema. SimpleDateFormat nos ofrece dos formas de pintar o parsear la zona horaria: «z», que es una representación textual larga y no nos sirve, y «Z» que es más corta y casi nos sirve. El «casi» es porque SimpleDateFormat representa la zona horaria con como la diferencia con GMT (Greenwich) en formato «+/-HHmm», es decir, el horario peninsular de invierno es «+0100». Pero en el estándar XML, las horas y minutos de diferencia están separados por dos puntos («:»), de forma que el ejemplo anterior se representaría como «+01:00».

Si la zona horaria va a ser siempre GMT, podemos aprovecharnos de que en el estándar XML, dicha zona horaria se representa como «Z», y hacer lo siguiente:


    public String toXmlString(Date date) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        return sdf.format(date);
    }

Pero si debemos usar otras zonas, y no podemos usar alguna librería más adecuada (el no uso de determinadas librerías puede ser un dato del problema), no nos queda más remedio que implementar algo más manual.

22.10.13

Un XML no es un texto, es un binario

O tal vez debería matizar el título y decir que un XML no es un fichero de texto plano, y que habría que tratarlo a la hora de guardar y leer de disco, red, base de datos, o de donde sea, como un binario. Claro que entonces quedaría un título muy largo.

El motivo por el que hago esta afirmación tan tajante, tiene que ver con lo que expliqué hace dos posts: el famoso encoding. Como recordaréis, comentaba que la representación en bytes de un texto, depende del encoding utilizado. Leer un fichero de texto con la codificación correcta es vital para que no aparezcan símbolos inusuales en lugar de vocales acentuadas o nuestra querida eñe. Y esa codificación, si no la indicamos explícitamente en nuestra aplicación, el JRE usará la que tenga el sistema sobre el que corre. Así, un fichero de texto guardado en Windows, podría leerse de forma incorrecta en Linux, y viceversa, si usamos el encoding por defecto en ambos sistemas.

Con un XML no tenemos ese problema. El propio formato incluye una forma de especificar la codificación en el prolog (la cabecera) mediante el atributo encoding. Por ejemplo:


<?xml version="1.0" encoding="utf-8"?>
<ejemplo>
  ...
  Y aquí irá todo lo demás
  ...
</ejemplo>

Tanto el atributo encoding como el propio prolog son opcionales. Pero no hay problema, ya que el estándar establece mecanismos alternativos para determinar la codificación del XML, como la presencia de un BOM al principio. Y si no existe ninguna forma de determinar la codificación, se asume UTF-8 por defecto. Entre los mecanismos alternativos para determinar la codificación, no se tiene en cuenta la codificación de la plataforma. Es decir, un fichero XML puede viajar por varios sistemas diferentes, y todos deben usar la misma codificación para entenderlo, independientemente de la codificación que use el sistema para otros menesteres. Cualquier aplicación que lea el XML de ejemplo que he puesto antes, debería usar UTF-8 sí o sí.

Esto es fantástico, ¿no? ¿Dónde está entonces el problema? Pues ocurre que, dado que un XML es en el fondo texto entendible por un ser humano, hay desarrolladores que cometen el error de considerarlo como cualquier otro fichero de texto, y usan subclases de Reader y Writer, o incluso String para operar con él. Y eso es una bomba de relojería. Las clases anteriores interpretarán los bytes subyacentes con un encoding (el de la plataforma, o uno que se haya especificado de forma explícita) que no tiene por qué coincidir con el del XML. Si la codificación usada es la misma, pues no pasa nada. Pero un día, ya no coinciden (se migra la aplicación a otro entorno, se usan XMLs con otras codificaciones), y empiezan a aparecer caracteres raros, o peor aún, el parser es muy estricto y lanza una excepción si encuentra secuencias de bytes no válidas en UTF-8. Y entonces, alguien dice la gran frase «pero si esto siempre ha funcionado ¿por qué no funciona ahora?».

Para evitar este problema, basta con tener siempre en mente lo siguiente: hay que tratar un XML como si fuera un binario, y nunca como texto. Así, a la hora de leer un fichero, independientemente de la librería y parser utilizados, hay que usar aquellos métodos que pidan un InputStream, y nunca los que pidan un Reader. Para escribir un XML creado por nosotros, hay que huir de los métodos que usen un Writer como de la peste, y abrazar los que usen un OutputStream. La misma consideración hay que tener si guardamos XMLs en una base de datos relacional, por ejemplo. Nada de VARCHAR, CLOB o similares; debemos usar un BLOB. Y si llegado el caso tuviéramos que tener un XML en crudo en memoria, y usarlo como argumento o retorno de un método, debemos declararlo siempre como byte[], y nunca como String.

Cuando se usan XMLs muy pequeños (con pocos elementos y textos muy limitados), uno tiene la tentación de generarlos e interpretarlos «a pelo», sin tener que pasar por librerías sofisticadas. Por ejemplo, si tenemos que generar un XML como


<mensaje>Esto es un mensaje corto</mensaje>

parece un poco excesivo usar JAXB. Es mucho más simple generarlo a base de concatenar cadenas. Pero en este caso, el resultado final debe ser siempre un array de bytes, controlando nosotros (y no la plataforma) el encoding usado. Por ejemplo:


    public byte[] generarXml(String texto) throws UnsupportedEncodingException {
        return  ("<mensaje>" + texto + "</mensaje>").getBytes("UTF-8");
    }

puesto que como ya he comentado, si no se incluye prolog, la codificación por defecto es UTF-8 (y sí, el código es mejorable, pero se trata sólo de un sencillo ejemplo).

El caso contrario, interpretar un XML, no importa lo simple que pueda ser, creo que siempre es preferible el uso de un parser en condiciones. Pensad que sólo para averiguar el encoding, hay que hacer una primera lectura para buscar el prolog y su atributo encoding (si existen), y luego volver a leer otra vez con la codificación adecuada. Parece un trabajo que sólo se justificaría si tenemos unas limitaciones determinadas de memoria o tamaño de la aplicación (o algún otro dato del problema).