6.4.16

SQLException ORA-01843 al migrar a Java 7

Hace mucho que no escribo nada aquí, lo que es una buena señal, ya que eso quiere decir que no me he encontrado con problemas que merezcan contarse. Hasta ahora.

Recientemente, tuve que migrar una aplicación web que estaba ejecutándose con Java 5, a un servidor con Java 7. Uno podría pensar que debe ser una tarea trivial. Después de todo, se supone que uno de los eslóganes de Java es el famoso WORA, y además, Java 7 debería ser compatible hacia atrás (no introduce nuevas palabras clave, como sucedió con Java 5). Sin embargo, tras arrancar la aplicación y navegar un poco por ella, en seguida saltó un error: Una java.sql.SQLException con el siguiente mensaje:


ORA-01843: not a valid month

Obviamente, la aplicación utilizaba una base de datos Oracle. Pero la base de datos seguía siendo la misma. Es más, el servidor de aplicaciones (un Weblogic 10.3) también era el mismo. La máquina física (un servidor Solaris) era la misma. Todo era exactamente igual que antes, salvo la versión del JDK. ¿Qué estaba ocurriendo?

Buscando en la documentación de Oracle, es fácil averiguar que el error en cuestión indica un problema en el formato de la fecha, cuando se ejecuta una sentencia contra la base de datos. Concretamente, cuando la parte correspondiente al mes no es un literal correcto. Buceando en el código, pude comprobar que, efecticamente, el error solo aparecía cuando había fechas involucradas en alguna query.

Hay que explicar que la aplicación era bastante vieja, y que no utilizaba ningún tipo de ORM como Hibernate, ni nada parecido. Los accesos a base de datos se hacían «a pelo», usando directamente el API JDBC, sin ninguna librería de apoyo. Las queries estaban definidas dentro del código, si bien, al menos se había tenido la prudencia de que fueran parametrizadas (es decir, no se construia la query concatenando directamente variables). En algunos casos, cuando había fechas involucradas, se utilizaban las funciones TO_CHAR y TO_DATE para formatear el dato, con un patrón específico dentro de la query. Pero en otros, se confiaba ciegamente en que el formato por defecto era DD/MM/YYYY. Y en esas queries era donde saltaba la excepción.

Así que el motivo el problema era bastante evidente. El formato por defecto de las fechas, ya no era el esperado. Pero ¿por qué? Como ya he dicho, sólo había cambiado el JDK. La aplicación (código y librerías) era la misma, el servidor era el mismo, la base de datos era la misma...

Lo que ocurría era lo siguiente: En Java 7, la determinación del Locale por defecto, cambia con respecto a versiones anteriores. Está reportado como bug, pero parece ser que en realidad es una feature (como los grumos del Cola Cao). Lo que importa es que al arrancar la JVM, el Locale por defecto quedaba establecido a inglés americano, en vez de a castellano español. Eso era lo que había cambiado el formato por defecto de la fecha.

Por supuesto, una solución era modificar todas las queries afectadas, y explicitar un formato de fecha concreto con TO_DATE o TO_CHAR, pero eso suponía mucho trabajo (era una aplicación enorme), y además se quería evitar tocar código si era posible (era un dato del problema). Así que la única solución era establecer el locale por defecto de la JVM a castellano español, que era como estaba antes.

En un servidor Unix, normalmente se hace estableciendo la variable de entorno LANG. Pero eso no funciona con la Java 7 (al menos, no con un Weblogic 10 o un Tomcat 7). Así que lo que hay que hacer es establecer las propiedades de sistema user.language y user.country a nuestro gusto, pasándoselas en el arranque de la aplicación. En mi caso, tuve que añadir lo siguiente en el script de arranque:


-Duser.language=es -Duser.country=ES

Es muy importante hacer notar que hay que proporcionar ambas propiedades. Uno podría estar tentado de indicar únicamente el lenguaje, pero si no se hace lo mismo con el país, el locale no se establece correctamente.

18.9.14

Cómo tener una página estática de inicio en Blogger, y que funcione en móviles

No creo que sea necesario decir que Blogger es un servicio gratuito de Google para publicar y mantener un blog. Una de sus características es que ofrece la posibilidad de tener páginas estáticas, que no forman parte de ningún post. Ahí se puede poner, por ejemplo, una página de introducción al blog, una sobre el autor... en fin, lo que uno quiera. En ocasiones, surge la necesidad de que una de esas páginas sea la home page, es decir, la página de inicio. La que se muestra cuando uno entra sin especificar nada (sólo con el nombre de dominio). Blogger usa una home page consistente en el listado de los últimos posts, sin muchas opciones para cambiar ese comportamiento.

Buscando por la red, uno puede encontrar fácilmente cómo hacer que una de esas páginas estáticas sea la home page del blog. El método más popular (por su sencillez de concepto) es utilizar las opciones de redirección que ofrece Blogger. Básicamente consiste en «hacer trampa», y decirle a Blogger que redireccione «/» a la página deseada, y luego añadir un enlace para ir a la lista de entradas. No voy a explicar aquí cómo se hace, ya que se pueden encontrar fácilmente muchas páginas al respecto (por ejemplo Creating a Blogger Static Home Page, que creo que es la que lo mejor explica).

Pero este sistema tiene un serio problema: hace imposible acceder al blog desde determinados dispositivos móviles. Resulta que cuando el servidor de Blogger detecta que se está accediendo desde un dispositivo móvil, hace una redirección a la misma página, pero añadiendo como parámetro en la query string, m=1. Este parámetro es utilizado por el motor de plantillas, de forma que se puede decidir mostrar cosas diferentes dependiendo de si el usuario está accediendo con un ordenador o con un móvil. Esto parece una buena idea, pero tal y como está implementado, colisiona con la redirección que configuremos nosotros en el blog. El resultado es que cuando el usuario accede desde un dispositivo móvil (o al menos, desde algunos de ellos), Blogger le redirecciona a otra URL. Pero esa URL a su vez le redirecciona a la anterior, que a su vez vuelve a redireccionar a la siguiente... y así caemos en un bucle infinito. Afortunadamente, la mayoría de los navegadores son capaces de detectarlo, y en vez de ralentizarnos el terminal con una ejecución que no se acaba nunca, muestran un mensaje de error al usuario.

En los tiempos que corren, uno no puede permitir que su web no se vea bien en un móvil. No digamos ya, que no se vea en absoluto. Me topé con este problema al hacer la web promocional de la novela «El viaje del Argos: Las memorias de Klatuu», ya que quería que el blog fuera algo secundario, y la home page fuera una página fija con la presentación del libro. Así que tras darle unas vueltas a la cabeza y experimentar un poco, implementé una solución con JavaScript.

La solución más básica consiste simplemente en redireccionar mediante JavaScript a la página deseada, sólo cuando se está cargando la home que ofrece Blogger, es decir, cuando se accede a «/»:


<b:if cond='data:blog.url == data:blog.homepageUrl'>
  <script>
    window.location="<data:blog.homepageUrl/>p/inicio.html"
  </script>
</b:if>

En este caso, la página a la que queremos redirigir es /p/inicio.html, y el bloque JavaScript sólo se envía al navegador, si la URL solicitada es la de la home (la de Blogger, no la nuestra). Hay que decir que el lenguaje que usa el motor de plantillas de Blogger, no está bien y completamente documentado en ningún sitio, y hay que recurrir a una mezcla de autoaprendizaje, búsqueda por la red, y ensayo y error (un buen punto de inicio es la ayuda de Blogger).

Fijaos que con este fragmento, redirigimos a nuestra página estática cada vez que alguien va a «/». Como en algún momento querremos mostrar la lista de entradas, necesitamos hacer algo más. Una solución es la que se ofrece habitualmente junto con la de la redirección de Blogger, que es poner un enlace a «/index.html», a «/search», o «trampas» similares. Pero como son características no documentadas, yo he preferido otra vía. Fijáos que la redirección sólo es necesaria cuando alguien entra desde otro sitio. Una vez el usuario está navegando dentro de nuestro blog, ya no es necesaria. Y eso podemos saberlo con la cabecera HTTP referer, que es accesible desde JavaScript:


<b:if cond='data:blog.url == data:blog.homepageUrl'>
  <script>
    if (!(document.referrer && 
          document.referrer.indexOf("<data:blog.homepageUrl/>") >= 0)) {
      window.location="p/inicio.html"
    }
  </script>
</b:if>

Así, sólo se realiza la redirección cuando no aparece el dominio de nuestro blog en la cabecera referer (la etiqueta <data:blog.homepageUrl/> devuelve la home por defecto de Blogger, esto es, nuestro dominio seguido de «/»).

Todavía queda un detalle. Cuando estamos preparando una entrada en Blogger, y le damos al botón de vista previa, no se manda ninguna cabecera referer, y por algún detalle de implementación que desconozco, el motor de plantillas considera que se está accediendo a «/», por lo que en vez de ver la vista previa de nuestra entrada, veremos la página estática que hemos definido como punto de entrada. Para evitarlo, hay que añadir una condición adicional al if JavaScript, para que descarte que la página sea una vista previa:


<b:if cond='data:blog.url == data:blog.homepageUrl'>
  <script>
    if(!(document.referrer && 
          document.referrer.indexOf("<data:blog.homepageUrl/>") >= 0) &&
          !(document.URL && document.URL.indexOf("post-preview")>= 0)) {
      window.location="p/inicio.html"
    }
  </script>
</b:if>

Finalmente, quiero recordaros que la plantilla de Blogger es un XML, por lo que para evitar problemas, debéis usar en el bloque JavaScript, las entidades XML correspondientes a los caracteres «&», «>», «<» y «"» (salvo en las etiquetas que queramos que el motor de plantillas evalue):


<b:if cond='data:blog.url == data:blog.homepageUrl'>
  <script>
    if(!(document.referrer &amp;&amp; 
          document.referrer.indexOf(&quot;<data:blog.homepageUrl/>&quot;) &gt;= 0) &amp;&amp;
          !(document.URL &amp;&amp; document.URL.indexOf(&quot;post-preview&quot;)&gt;= 0)) {
      window.location=&quot;p/inicio.html&quot;
    }
  </script>
</b:if>

Queda algo más críptico, pero leyendo con calma se entiende.

Si tenéis curiosidad por verlo en funcionamiento, podéis pasaros por la página que os he mencionado y navegar mientras observáis la bara de direcciones.

Actualización: La web oficial de mi novela se encuentra ahora alojada de Github, pero he mantenido la versión de Blogger para que podáis seguir viendo esta técnica.

29.7.14

Las limitaciones de CSS en un EPUB

EPUB es un formato abierto de libro electrónico desarrollado por el IDPF. Una de sus características es que el texto de libro se encuentra en XHTML (también puede estar en formato DTBook, pero de momento, todos los que he leído usan XHTML), y se puede (y debe) usar CSS para aplicar estilos. Esto hace que una persona con experiencia en el mundo HTML, pueda maquetar libros en formato EPUB, con un pequeño aprendizaje adicional.

Un EPUB no es una página web, por lo que hay algunas recomendaciones oficiales, como el evitar la pseudoclase :hover o limitaciones a la hora de usar la propiedad position.

El problema es que nos encontramos en una situación similar a la de los 90 con la web. Por un lado, aunque el estándar oficial va por la versión 3, aún quedan muchos dispositivos en manos de los usuarios, que sólo soportan la versión 2. Por otro, hay varias aplicaciones para dispositivos móviles (sobre todo en Android) que permiten leer EPUB, con bastantes limitaciones. Y si bien, para tener una experiencia agradable es preferible el uso de un eReader con tinta electrónica, la gratuidad de muchas de estas aplicaciones hace que haya gente que se decante por leer en su tablet o móvil.

No hay en la web (o al menos no he encontrado) una lista de limitaciones en el soporte CSS de distintos dispositivos, así que os detallo aquí las cosas que me he ido encontrando de forma experimental.

Para empezar, no todos los dispositivos tienen un soporte completo de los selectores CSS. Algo tan simple como:


p {
  text-indent: 0;
}

p+p {
  text-indent: 2em;
}

que nos permitiría que todos los párrafos estén sangrados excepto el primero (práctica habitual en la narrativa), no es entendible por todos los dispositivos. Tampoco está garantizado que funcionen las pseudoclases (como :first-child) ni los pseudoelementos (como :first-letter). Pero es más, ni siquiera algo tan básico como el anidamiento de elementos, o el uso de varios selectores separados por comas, funcionará en todos sitios. Olvidáos pues de cosas como:


.chapter p { 
}

.title, .subtitle, .author {
}

Posiblemente un buen lector los entendería, pero alguna aplicación de Android no lo hará. La técnica más segura es limitarse a un único selector por declaración, con un único elemento (con o sin clase), lo que nos obliga a añadir muchas clases en nuestro HTML, y a repetir código en la CSS, para selectores que deben tener el mismo estilo. Por ejemplo:


h1 {
  font-family: Arial, sans-serif; 
  font-weight: bold;
  font-size: 26px;
}

h2 {
  font-family: Arial, sans-serif; 
  font-weight: bold;
  font-size: 20px;
}

p {
  text-indent: 2em;
}

p.first {
  text-indent: 0;
}

p.in-copyright-page {
}

img.in-copyright-page {
}

ul.in-copyright-page {
}

Más cosas. Según el estándar CSS, cuando el valor de una propiedad de medida (como margin, padding) es cero, no es necesario especificar ninguna unidad. Parece algo lógico, ya que 0em y 0px es en realidad lo mismo: cero. Pero existen aplicaciones o dispositivos lectores de EPUB, que si no se especifica siempre una unidad, ignorarán el valor. Así que, para curarse en salud, lo mejor es especificarla, aunque el valor numérico sea cero.

Por último, algunos dispositivos no soportan las propiedades compuestas o multivalor, es decir, aquellas propiedades que en realidad están especificando varios valores de forma compacta. Por ejemplo:


p {
  margin: 0em;
}

puede no funcionar en algún dispositivo. Así que, aunque tedioso, es más seguro optar por la versión más larga de especificar lo mismo:


p {
  margin-top: 0em;
  margin-right: 0em;
  margin-bottom: 0em;
  margin-left: 0em;
}

Vuelvo a repetir que esto son limitaciones que he descubierto en algunos lectores (dispositivos o aplicaciones). Un buen eReader seguramente soportará correctamente todo el estándar CSS. Pero si queremos que nuestro EPUB se vea correctamente en la mayor cantidad de lectores posibles, es conveniente tener estas restricciones en mente.