tag:blogger.com,1999:blog-45150788477685402862024-02-08T19:04:31.798+01:00Los apuntes de adeteranApuntes acerca del desarrollo de aplicaciones web sobre Java.Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.comBlogger14125tag:blogger.com,1999:blog-4515078847768540286.post-7277256901662192322016-04-06T17:55:00.000+02:002016-04-06T17:55:51.658+02:00SQLException ORA-01843 al migrar a Java 7<p>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.</p>
<p>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 <abbr title="Write Once Run Anywere">WORA</abbr>, 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 <code>java.sql.SQLException</code> con el siguiente mensaje:</p>
<pre class="prettyprint"><code>
ORA-01843: not a valid month</code></pre>
<p>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?</p>
<p>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 <span lang="en">query</span>.</p>
<p>Hay que explicar que la aplicación era bastante vieja, y que no utilizaba ningún tipo de <abbr title="Object Relational Mapping">ORM</abbr> 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 <span lang="en">queries</span> 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 <span lang="en">query</span> concatenando directamente variables). En algunos casos, cuando había fechas involucradas, se utilizaban las funciones <code>TO_CHAR</code> y <code>TO_DATE</code> para formatear el dato, con un patrón específico dentro de la <span lang="en">query</span>. Pero en otros, se confiaba ciegamente en que el formato por defecto era <code>DD/MM/YYYY</code>. Y en esas <span lang="en">queries</span> era donde saltaba la excepción.</p>
<p>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...</p>
<p>Lo que ocurría era lo siguiente: En Java 7, la determinación del <code><a href="http://docs.oracle.com/javase/7/docs/api/java/util/Locale.html">Locale</a></code> por defecto, cambia con respecto a versiones anteriores. Está <a href="http://bugs.java.com/bugdatabase/view_bug.do?bug_id=7073906">reportado</a> como <span lang="en">bug</span>, pero parece ser que en realidad es una <span lang="en">feature</span> (como los grumos del Cola Cao). Lo que importa es que al arrancar la JVM, el <code>Locale</code> 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.</p>
<p>Por supuesto, una solución era modificar todas las <span lang="en">queries</span> afectadas, y explicitar un formato de fecha concreto con <code>TO_DATE</code> o <code>TO_CHAR</code>, 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 <a href="http://apuntesadeteran.blogspot.com.es/2013/01/es-un-dato-del-problema.html">un dato del problema</a>). Así que la única solución era establecer el <code>locale</code> por defecto de la JVM a castellano español, que era como estaba antes.</p>
<p>En un servidor Unix, normalmente se hace estableciendo la variable de entorno <code>LANG</code>. 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 <code>user.language</code> y <code>user.country</code> 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:</p>
<pre class="prettyprint"><code>
-Duser.language=es -Duser.country=ES</code></pre>
<p>Es muy importante hacer notar que hay que proporcionar <b>ambas</b> propiedades. Uno podría estar tentado de indicar únicamente el lenguaje, pero si no se hace lo mismo con el país, el <code>locale</code> no se establece correctamente.</p>
Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-39111261776329667842014-09-18T23:20:00.000+02:002017-01-31T23:34:14.641+01:00Cómo tener una página estática de inicio en Blogger, y que funcione en móviles<p>No creo que sea necesario decir que <a href="https://www.blogger.com">Blogger</a> 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 <span lang="en">post</span>. 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 <span lang="en">home page</span>, 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 <span lang="en">home page</span> consistente en el listado de los últimos <span lang="en">posts</span>, sin muchas opciones para cambiar ese comportamiento.</p>
<p>Buscando por la red, uno puede encontrar fácilmente cómo hacer que una de esas páginas estáticas sea la <span lang="en">home page</span> 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 «<code>/</code>» 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 <a href="http://www.bloggertipspro.com/2012/06/creating-blogger-static-home-page.html">Creating a Blogger Static Home Page</a>, que creo que es la que lo mejor explica).</p>
<p>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 <span lang="en">query string</span>, <code>m=1</code>. 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.</p>
<p>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 «<a href="http://lasmemoriasdeklatuu.blogspot.com.es">El viaje del Argos: Las memorias de Klatuu</a>», ya que quería que el blog fuera algo secundario, y la <span lang="en">home page</span> 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.</p>
<p>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 <span lang="en">home</span> que ofrece Blogger, es decir, cuando se accede a «<code>/</code>»:</p>
<pre class="prettyprint lang-css"><code>
<b:if cond='data:blog.url == data:blog.homepageUrl'>
<script>
window.location="<data:blog.homepageUrl/>p/inicio.html"
</script>
</b:if></code></pre>
<p>En este caso, la página a la que queremos redirigir es <code>/p/inicio.html</code>, y el bloque JavaScript sólo se envía al navegador, si la URL solicitada es la de la <span lang="en">home</span> (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 <a href="https://support.google.com/blogger/answer/46995">ayuda de Blogger</a>).</p>
<p>Fijaos que con este fragmento, redirigimos a nuestra página estática cada vez que alguien va a «<code>/</code>». 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 «<code>/index.html</code>», a «<code>/search</code>», 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 <a href="http://es.wikipedia.org/wiki/Referer_(Cabecera_HTTP)">cabecera HTTP <code>referer</code></a>, que es accesible desde JavaScript:</p>
<pre class="prettyprint lang-css"><code>
<b:if cond='data:blog.url == data:blog.homepageUrl'>
<script>
if (!(document.referrer &&
document.referrer.indexOf("<data:blog.homepageUrl/>") >= 0)) {
window.location="<data:blog.homepageUrl/>p/inicio.html"
}
</script>
</b:if></code></pre>
<p>Así, sólo se realiza la redirección cuando no aparece el dominio de nuestro blog en la cabecera <code>referer</code> (la etiqueta <code><data:blog.homepageUrl/></code> devuelve la <span lang="en">home</span> por defecto de Blogger, esto es, nuestro dominio seguido de «<code>/</code>»).</p>
<p>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 <code>referer</code>, y por algún detalle de implementación que desconozco, el motor de plantillas considera que se está accediendo a «<code>/</code>», 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 <code>if</code> JavaScript, para que descarte que la página sea una vista previa:</p>
<pre class="prettyprint lang-css"><code>
<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="<data:blog.homepageUrl/>p/inicio.html"
}
</script>
</b:if></code></pre>
<p>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 «<code>&</code>», «<code>></code>», «<» y «<code>"</code>» (salvo en las etiquetas que queramos que el motor de plantillas evalue):</p>
<pre class="prettyprint lang-css"><code>
<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;<data:blog.homepageUrl/>p/inicio.html&quot;
}
</script>
</b:if></code></pre>
<p>Queda algo más críptico, pero leyendo con calma se entiende.</p>
<p>Si tenéis curiosidad por verlo en funcionamiento, podéis pasaros por <a href="http://lasmemoriasdeklatuu.blogspot.com.es">la página que os he mencionado</a> y navegar mientras observáis la bara de direcciones.</p>
<p><strong>Actualización:</strong> La <a href="http://elviajedelargos.lasmemoriasdeklatuu.info">web oficial de mi novela</a> se encuentra ahora alojada de <a href="http://github.com/">Github</a>, pero he mantenido la versión de Blogger para que podáis seguir viendo esta técnica.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com12tag:blogger.com,1999:blog-4515078847768540286.post-56367595310561691242014-07-29T20:06:00.003+02:002014-07-29T20:06:33.458+02:00Las limitaciones de CSS en un EPUB<p><a href="http://idpf.org/epub">EPUB</a> es un formato abierto de libro electrónico desarrollado por el <a href="">IDPF</a>. 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.</p>
<p>Un EPUB no es una página web, por lo que hay algunas <a href="http://www.idpf.org/accessibility/guidelines/content/style/reference.php">recomendaciones oficiales</a>, como el evitar la pseudoclase <code>:hover</code> o limitaciones a la hora de usar la propiedad <code>position</code>.</p>
<p>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.</p>
<p>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.</p>
<p>Para empezar, no todos los dispositivos tienen un soporte completo de los selectores CSS. Algo tan simple como:</p>
<pre class="prettyprint lang-css"><code>
p {
text-indent: 0;
}
p+p {
text-indent: 2em;
}</code></pre>
<p>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 <code>:first-child</code>) ni los pseudoelementos (como <code>:first-letter</code>). 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:</p>
<pre class="prettyprint lang-css"><code>
.chapter p {
}
.title, .subtitle, .author {
}</code></pre>
<p>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:</p>
<pre class="prettyprint lang-css"><code>
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 {
}</code></pre>
<p>Más cosas. Según el estándar CSS, cuando el valor de una propiedad de medida (como <code>margin</code>, <code>padding</code>) es cero, no es necesario especificar ninguna unidad. Parece algo lógico, ya que <code>0em</code> y <code>0px</code> 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.</p>
<p>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>
<pre class="prettyprint lang-css"><code>
p {
margin: 0em;
}</code></pre>
<p>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>
<pre class="prettyprint lang-css"><code>
p {
margin-top: 0em;
margin-right: 0em;
margin-bottom: 0em;
margin-left: 0em;
}</code></pre>
<p>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.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com2tag:blogger.com,1999:blog-4515078847768540286.post-16521470350306726702014-07-05T12:24:00.001+02:002014-07-05T12:24:32.790+02:00NoClassDefFoundError cuando la clase sí que se encuentra<p>La documentación oficial de la clase <a href="http://docs.oracle.com/javase/7/docs/api/java/lang/NoClassDefFoundError.html"><code>java.lang.NoClassDefFoundError</code></a> nos dice que esta excepción se lanza cuando la máquina virtual intenta cargar la definición de una clase, pero esta definición no se encuentra. Esto indica que cuando el programa se compiló, la clase estaba presente, pero en la ejecución ya no está.</p>
<p>Si al ejecutar una aplicación nos salta esta excepción, al leer esto enseguida pensamos en problemas de <code>classpath</code> o de empaquetamiento, y nos ponemos como locos a comprobar si el jar está bien construido, si están todos los jars, si tenemos bien definido el <code>classpath</code>, etc. Pero a veces, por mucho que busquemos no encontramos ningún error ahí. Es más, es especialmente desconcertante cuando la clase supuestamente desaparecida, está en el mismo jar que la clase que la busca y no la encuentra.</p>
<p>¿Qué puede estar ocurriendo? Bien, hay un motivo por el que la máquina virtual puede lanzar una <code>java.lang.NoClassDefFoundError</code>, aunque encuentre la clase: durante la inicialización estática de la misma. Imaginemos la siguiente clase:</p>
<pre class="prettyprint"><code>
public class SomeClass {
public static final int SOME_CONSTANT = precalculateSomeValue();
static {
initializeSomething();
}
// (...)
}</code></pre>
<p>Si durante la ejecución del método <code>precalculateSomeValue()</code> o de <code>initializeSomething()</code>, se lanzara una excepción, la clase que hace referencia a <code>SomeClass</code> (y que ha «disparado» la inicialización), lanzará una <code>NoClassDefFoundError</code>. Fijaos que en este caso, la clase sí que ha sido localizada correctamente por la máquina virtual. El problema no es que no la haya encontrado, sino que no la ha podido inicializar.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com3tag:blogger.com,1999:blog-4515078847768540286.post-49288352404161538312014-04-15T23:11:00.000+02:002014-04-15T23:11:01.413+02:00Uso de jarsigner con un Provider propio<p>En el <a href="http://apuntesadeteran.blogspot.com.es/2014/03/autenticacion-de-cliente-con.html">último <span lang="en">post</span></a> expliqué cómo usar un <code>Provider</code> propio para autenticarse con certificado de cliente en una conexión HTTPS. El siguiente paso era inevitable: queríamos usar la misma infraestructura para firmar ficheros jar (requisito indispensable si queremos desplegar un <span lang="en">applet</span> con ciertos privilegios, por ejemplo).</p>
<p>Como sabéis, el JDK nos ofrece una herramienta de línea de comandos llamada <code>jarsigner</code>. Es la única opción que tenemos para firmar un jar, ya que no se ofrece un API para poder hacerlo de forma programática. La herramienta está pensada sobre todo para utilizar un <code>keystore</code> Java o un fichero PKCS#12, donde estaría la clave privada. Pero ¿qué hacemos si la única forma de acceder a la misma es mediante un <code>Provider</code> propio.</p>
<p>Nuevamente la solución pasa por estudiar primero cómo se haría con un PKCS#11. La <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/security/p11guide.html#KeyToolJarSigner">documentación oficial de Oracle</a> es un poco parca al respecto, pero suficiente para hacernos una idea. Al igual que ocurría en el <span lang="en">post</span> anterior, si nuestra implementación no está basada en un fichero en el disco donde se encuentran las entradas, como opción <code>-keystore</code> se debe pasar el literal "NONE". La opción <code>-storetype</code> deberá tener el nombre que le hayamos dado a nuestro propio tipo de <code>KeyStore</code>. Y aquí viene lo importante: deberemos usar la opción <code>-providerClass</code> con el nombre completo de nuestra implementación de <code>Provider</code>, incluyendo el paquete. Un ejemplo sencillo podría ser el siguiente:</p>
<pre class="prettyprint"><code>
jarsigner -keystore NONE -storetype MyType -storepass password -providerClass my.package.MyProvider /path/to/app.jar alias
</code></pre>
<p>Pero para que nos funcione, antes debemos hacer algo muy importante. ¿Cómo sabe la herramienta <code>jarsigner</code> dónde está el jar con nuestra implementación de <code>Provider</code>? Pues en realidad, no lo sabe. Ni tampoco se lo podemos decir. La herramienta <code>jarsigner</code> no tiene una opción <code>-classpath</code> o similar con que pasarle las rutas con los jars que debe usar. Así que nuestra única posibilidad es añadir nuestra implementación a la instalación del JDK, en la carpeta destinada a extender el JRE: <code>$JAVA_HOME/jre/lib/ext/</code>. No hay que perder de vista que tal vez necesitemos permisos de administrador para ello.</p>
<p>Además, hay tener en cuenta las implicaciones de esta acción: cualquier aplicación Java que se ejecute con esa instalación, podrá usar nuestra implementación de <code>Provider</code>. Eso no quiere decir que nuestra implementación pueda usarse por accidente si no queremos. Añadir nuestros jars a la carpeta de extensión, sólo quiere decir que sus clases estarán disponibles, como si se trataran de las del propio JRE. Pero si una aplicación quiere hacer uso de nuestro <code>Provider</code>, necesitará instalarlo con la conocida llamada a <a href="http://docs.oracle.com/javase/7/docs/api/java/security/Security.html#addProvider(java.security.Provider)">java.security.Security.addProvider(java.security.Provider provider)</a>. Si queremos que nuestro <code>Provider</code> esté siempre disponible como el resto de los que trae el JDK, sin necesidad de añadirlo dinámicamente en nuestra aplicación, debemos configurarlo en el fichero <code>$JAVA_HOME/jre/lib/security/java.security</code> (y hay que estar muy seguros de que realmente es eso lo que queremos).</p>
<p>Posiblemente, nuestra implementación necesite algún parámetro. En mi caso concreto, era la URL del servicio remoto. En el caso de la implementación PKCS#11 de Sun, es un fichero de configuración. Si a <code>jarsigner</code> le pasamos la opción <code>-providerArg</code>, el JDK buscará un constructor con un <code>String</code> como único argumento, y lo invocará usando el valor de la opción en cuestión.</p>
<p>Otro detalle muy importante, que nos puede dar sorpresas si no lo tenemos en cuenta. La herramienta usa una serie de algoritmos de firma por defecto, dependiendo del tipo de nuestra clave privada. Por ejemplo, para una clave RSA, el algoritmo por defecto es <code>SHA256withRSA</code> a partir de Java 7. Si nuestro <code>Provider</code> no implementa el algoritmo que <code>jarsigner</code> elija, se utilizará otro <code>Provider</code>, con resultados no deseados. Así que, o bien nos aseguramos de que nuestro <code>Provider</code> implemente los <a href="http://docs.oracle.com/javase/7/docs/technotes/tools/solaris/jarsigner.html#DefaultAlgs">algoritmos por defecto</a>, o bien utilizamos la opción <code>-sigalg</code>, indicando el algoritmo que queremos usar.</p>
<p>Así que nuestra llamada a <code>jarsigner</code> nos podría quedar algo parecido a esto (por claridad, he troceado el comando en varias líneas):</p>
<pre class="prettyprint"><code>
$HAVA_HOME/bin/jarsigner \
-keystore NONE \
-storetype MyKeyStoreType \
-storepass mypassword \
-providerClass my.own.package.MyProvider \
-providerArg "some string with some configuration" \
-sigalg SHA1withRSA \
/path/to/file/to/be/signed/app.jar \
alias-to-use
</code></pre>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-4272147837892663862014-03-11T14:52:00.000+01:002016-09-27T10:12:39.193+02:00Autenticación de cliente con certificado, usando un Provider propio<p>Realizar conexiones a un servidor HTTPS, únicamente con las clases que proporciona el JDK, es bastante sencillo. Hay mucha información, tanto en la red como en la propia documentación de Oracle. Si necesitamos autenticarnos con un certificado de cliente, que tengamos en un <span lang="en">keystore</span>, es también bastante sencillo. Con una simple búsqueda en Google averiguaremos que tenemos que ajustar determinadas propiedades del sistema, bien en el arranque de nuestra aplicación (con los parámetros <code>-D</code>), bien en nuestro código (con la clase <code><a href="http://docs.oracle.com/javase/7/docs/api/java/lang/System.html">System</a></code>). Por ejemplo, si nuestra pareja de clave privada y certificado está en fichero PKCS#12, en la ruta <code>/ruta/credenciales.p12</code>, y la contraseña del mismo es «clave», tendríamos que ajustarlas así:</p>
<pre class="prettyprint"><code>
javax.net.ssl.keyStore = /ruta/credenciales.p12
javax.net.ssl.keyStorePassword = clave
javax.net.ssl.keyStoreType = PKCS12
</code></pre>
<p>Esto nos valdría tanto para un keystore de Java (tipo JKS) como para un PKCS#12 (tipo PKCS12). La cosa se complica un poco si no tenemos nuestra clave privada en un fichero accesible, sino en un dispositivo criptográfico. Si tenemos una implementación PKCS#11 para el mismo, tampoco habría demasiado problema. Necesitamos configurar adecuadamente el <code>Provider</code> SunPKCS11, tal y como nos explica la <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/security/p11guide.html#P11Provider">documentación de Oracle</a>, y ajustar las siguientes propiedades de sistema:</p>
<pre class="prettyprint"><code>
javax.net.ssl.keyStore = NONE
javax.net.ssl.keyStorePassword = clave
javax.net.ssl.keyStoreType = PKCS11
</code></pre>
<p>Pero ¿qué tenemos que hacer en casos más particulares? Concretamente, ¿cómo usaríamos un <code>Provider</code> diferente a los del JDK?</p>
<p>Imaginemos la siguiente situación real: las claves y certificados están en una máquina remota, a la que se accede con un protocolo de red, y que tiene un API que permite obtener una lista de las claves y certificados, el certificado en sí, y realizar una firma digital de los datos que se le pasen. Es decir, una máquina que a todos los efectos se comporte como un hardware criptográfico (las claves privadas nunca salen de allí), pero que no tiene un interfaz PKCS#11. Para poder usar esta infraestructura desde Java, se implementó un <code><a href="http://docs.oracle.com/javase/7/docs/api/java/security/Provider.html">Provider</a></code> propio, que proporcionaba implementaciones de <code><a href="http://docs.oracle.com/javase/7/docs/api/java/security/KeyStore.html">KeyStore</a></code>, <code><a href="http://docs.oracle.com/javase/7/docs/api/java/security/PrivateKey.html">PrivateKey</a></code> y <code><a href="http://docs.oracle.com/javase/7/docs/api/java/security/Signature.html">Signature</a></code>. No entraré en los detalles de cómo implementar un <code>Provider</code>, ya que está bastante bien documentado en <a href="http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html">Java Cryptography Architecture (JCA) Reference Guide</a>.</p>
<p>¿Cómo usar esta implementación para autenticarnos en una conexión HTTPS? Fácil. Tenemos que actuar de forma similar al PKCS#11, pero utilizando los datos de nuestra implementación:</p>
<pre class="prettyprint"><code>
javax.net.ssl.keyStore = NONE
javax.net.ssl.keyStorePassword = clave (si fuera necesaria)
javax.net.ssl.keyStoreType = Nombre de nuestro nuevo tipo de KeyStore
javax.net.ssl.keyStoreProvider = Nombre de nuestro Provider
</code></pre>
<p>Hay unas consideraciones a tener en cuenta. Por un lado, si en nuestro <code>Provider</code> hemos definido un nuevo tipo de <code>KeyStore</code>, cuyo nombre no coincida con los de algún otro <code>Provider</code>, no necesitamos especificar la propiedad <code>javax.net.ssl.keyStoreProvider</code>, puesto que el nuestro será el único que lo proporcione.</p>
<p>Más importante. Debemos asegurarnos de que nuestra implementación acepte el algoritmo de firma <code>NONEwithRSA</code>. Como sabréis, una autenticación con certificado se basa en la firma de un token que genera la parte contraria. Pues bien, el JRE utiliza <code>NONEwithRSA</code> como algoritmo de firma en una conexión SSL o TLS. Si nuestra implementación no lo soporta, no podremos usarla.</p>
<p>Compliquemos un poco las cosas. Supongamos ahora que el <code>KeyStore</code> de nuestro <code>Provider</code>, no tiene una única entrada, sino varias. ¿Cómo sabe el JRE qué clave utilizar? Bueno, pues no lo sabe, y simplemente elige la primera que encaja (no necesariamente la primera que encuentra, ya que el certificado debe estar emitido por una CA que el servidor confíe). Si queremos especificar una clave concreta entre varias, debemos además implementar nuestro propio <code><a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/KeyManager.html">KeyManager</a></code>. Para ello, Oracle recomienda heredar de la clase <code><a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509ExtendedKeyManager.html">X509ExtendedKeyManager</a></code>. El método clave es <a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/X509KeyManager.html#chooseClientAlias(java.lang.String[], java.security.Principal[], java.net.Socket)"><code>chooseClientAlias</code></a>, que es al que se llama cuando el JRE necesita saber cuál de todas las entradas del <code>KeyStore</code> debe utilizar.</p>
<p>Pero ojo. No es suficiente con esto. Tal y como está diseñado todo el tinglado, debemos decirle de forma más explícita al JRE que use nuestro <code>KeyManager</code>. Para ello, debemos obtener una instancia de <code><a href="KeyManagerFactory">KeyManagerFactory</a></code> (también una implementación propia que deberemos configurar en nuestro <code>Provider</code>, y que devolverá nuestra implementación de <code>KeyManager</code>), usarla para inicializar un <code><a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLContext.html">SSLContext</a></code>, obtener de él una <code><a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/SSLSocketFactory.html">SSLSocketFactory</a></code>, y pasársela a la <code><a href="http://docs.oracle.com/javase/7/docs/api/javax/net/ssl/HttpsURLConnection.html">HttpsURLConnection</a></code> que respresenta nuestra conexión HTTPS, y que es la implementación que nos devolverá <code><a href="http://docs.oracle.com/javase/7/docs/api/java/net/URL.html#openConnection()">URL.openConnection()</a></code> si usamos el protocolo HTTPS.</p>
<p>Os dejo un pequeño ejemplo:</p>
<pre class="prettyprint"><code>
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("MyKeyManager", "MyProvider");
KeyStore keyStore = KeyStore.getInstance("MyKeyStore");
keyStore.load(null, null);
keyManagerFactory.init(keyStore, new char[0]);
SSLContext sslContext = SSLContext.getInstance("TSL");
sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
URL url = new URL("https://server.com/example");
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslSocketFactory());
</code></pre>
<p>He obviado todo lo relacionado con la implementación del <code>Provider</code> y su configuración, y he supuesto que no es necesaria ninguna contraseña.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-43210207895824078212014-02-03T22:37:00.000+01:002014-02-03T22:37:23.611+01:00Tablas que no se muestran con FOP 0.20<p><a href="http://xmlgraphics.apache.org/fop/">FOP</a> es la implementación de Apache del estándar <a href="http://www.w3.org/standards/techs/xsl">XSL</a> (también conocido como XSL-FO). Soporta varios formatos de salida, pero es habitual usarlo sólo para generar PDFs.</p>
<p>Las primeras versiones usables fueron las 0.20.4 y 0.20.5, allá por 2003. La siguiente <span lang="en">release</span> 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 <a href="http://apuntesadeteran.blogspot.com.es/2013/01/es-un-dato-del-problema.html">un dato del problema</a>.</p>
<p>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.</p>
<p>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).</p>
<p>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.</p>
<pre class="prettyprint"><code>
<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>
</code></pre>
<p>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:</p>
<pre class="prettyprint"><code>
<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>
</code></pre>
<p>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 <code>proportional-column-width</code>. La versión 0.20.4 de FOP tampoco soporta el uso de porcentajes para el ancho (sí se pueden usar <code>cm</code> o <code>pt</code>), 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 «%»).</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-79648635996731107562013-12-23T13:59:00.001+01:002013-12-23T13:59:56.725+01:00Pequeños «gotchas» de Date, Calendar y SimpleDateFormat en Java<p>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.</p>
<p>Primero voy a recordar lo más básico. Para la maquina virtual, la fecha y hora es un <code>long</code>, 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 <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Date.html">Date</a></code> y <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html">Calendar</a></code> son meros envoltorios con utilidades diversas.</p>
<p>Y ahora sí, vamos con los <span lang="en">gotchas</span>.</p>
<h4>Meses en <code>Calendar</code></h4>
<p>En la clase <code>Calendar</code>, los meses empiezan desde <b>cero</b>. 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 (<code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#JANUARY">JANUARY</a></code>, <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#FEBRUARY ">FEBRUARY</a></code>), pero es fácil olvidar este detalle y tener un disgusto. Sobre todo, cuando la clase <code><a href="http://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html">SimpleDateFormat</a></code> sí sigue el criterio intuitivo, y los meses empiezan con uno (enero es 1, febrero es 2, diciembre es 12).</p>
<h4><code>HOUR</code> vs. <code>HOUR_OF_DAY</code></h4>
<p><code>Calendar</code> nos ofrece muchas constantes para referirnos a las distintas partes de la fecha y hora, pero cuidado con ellas. La constante <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#HOUR">HOUR</a></code> 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</p>
<pre class="prettyprint"><code>
calendar.set(Calendar.HOUR, 6);
</code></pre>
<p>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 <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#HOUR_OF_DAY">HOUR_OF_DAY</a></code>. Por ejemplo:</p>
<pre class="prettyprint"><code>
calendar.set(Calendar.HOUR_OF_DAY, 6);
</code></pre>
<h4><code>DATE</code> no es lo que parece</h4>
<p>Otra constante engañosa: <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#DATE">DATE</a></code> es equivalente a <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#DAY_OF_MONTH">DAY_OF_MONTH</a></code> 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.</p>
<h4>Métodos iguales que no hacen lo mismo</h4>
<p>Tanto <code>Date</code> como <code>Calendar</code> tienen un método <code>getTime()</code>, pero ojo, porque devuelven cosas diferentes. El <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Date.html#getTime()">getTime()</a></code> de <code>Date</code> devuelve un <code>long</code> con el tiempo interno (los famosos milisegundos transcurridos desde las 00:00 GMT del 1 de enero de 1970), mientras que el <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#getTime()">getTime()</a></code> de <code>Calendar</code> devuelve un objeto <code>Date</code>. Si queremos el <code>long</code> con los milisegundos, debemos usar <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/Calendar.html#getTimeInMillis()">getTimeInMillis()</a></code></p>
<h4>Horas en <code>SimpleDateFormat</code></h4>
<p>Vamos con <code>SimpleDateFormat</code> 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: </p>
<pre class="prettyprint"><code>
SimpleDateFormat sdf = new SimpleDateFormat("hh:mm");
</code></pre>
<p>Nos devolverá 6:30, tanto si le pasamos un <code>Date</code> 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:</p>
<pre class="prettyprint"><code>
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
</code></pre>
<p><code>SimpleDateFormat</code> 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).</p>
<h4>No hay validaciones</h4>
<p>Un comportamiento curioso de <code>Calendar</code> y <code>SimpleDateFormat</code> 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 <code>long</code> 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).</p>
<h4>XML <code>dateTime</code></h4>
<p>Termino con algo que no es un «<span lang="en">gotcha</span>» sino una limitación. No hay forma de especificar un patrón para <code>SimpleDateFormat</code> que cumpla con el estándar XML, si queremos especificar la zona horaria. Si no especificamos la zona, basta con el siguiente patrón:</p>
<pre class="prettyprint"><code>
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
</code></pre>
<p>o incluso</p>
<pre class="prettyprint"><code>
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS");
</code></pre>
<p>si queremos llegar hasta el milisegundo. Pero si necesitamos especificar la zona horaria, tenemos un problema. <code>SimpleDateFormat</code> 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 <code>SimpleDateFormat</code> 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».</p>
<p>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:</p>
<pre class="prettyprint"><code>
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);
}
</code></pre>
<p>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 <a href="http://apuntesadeteran.blogspot.com.es/2013/01/es-un-dato-del-problema.html">un dato del problema</a>), no nos queda más remedio que implementar algo más manual.</p>
Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-75921990654605575242013-10-22T23:23:00.000+02:002013-10-22T23:23:02.573+02:00Un XML no es un texto, es un binario<p>O tal vez debería matizar el título y decir que un XML no es un fichero de texto <em>plano</em>, y que habría que tratarlo a la hora de guardar y leer de disco, red, base de datos, o de donde sea, <em>como</em> un binario. Claro que entonces quedaría un título muy largo.</p>
<p>El motivo por el que hago esta afirmación tan tajante, tiene que ver con lo que <a href="http://apuntesadeteran.blogspot.com.es/2013/06/abordando-problemas-de-encoding-en-java.html">expliqué hace dos <span lang="en">posts</span></a>: el famoso <span lang="en">encoding</span>. Como recordaréis, comentaba que la representación en bytes de un texto, depende del <span lang="en">encoding</span> 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 <span lang="en">encoding</span> por defecto en ambos sistemas.</p>
<p>Con un XML no tenemos ese problema. El propio formato incluye una forma de especificar la codificación en el <a href="http://www.w3.org/TR/xml/#sec-prolog-dtd" lang="en">prolog</a> (la cabecera) mediante el atributo <code>encoding</code>. Por ejemplo:</p>
<pre class="prettyprint"><code>
<?xml version="1.0" encoding="utf-8"?>
<ejemplo>
...
Y aquí irá todo lo demás
...
</ejemplo>
</code></pre>
<p>Tanto el atributo <code>encoding</code> como el propio <span lang="en">prolog</span> 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 <a href="http://www.unicode.org/faq/utf_bom.html#BOM"><abbr title="Byte Order Mark">BOM</abbr></a> 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, <strong>no</strong> 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í.</p>
<p>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 <code>Reader</code> y <code>Writer</code>, o incluso <code>String</code> para operar con él. Y eso es una bomba de relojería. Las clases anteriores interpretarán los bytes subyacentes con un <code>encoding</code> (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 <span lang="en">parser</span> 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?».</p>
<p>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 <span lang="en">parser</span> utilizados, hay que usar aquellos métodos que pidan un <code>InputStream</code>, y nunca los que pidan un <code>Reader</code>. Para escribir un XML creado por nosotros, hay que huir de los métodos que usen un <code>Writer</code> como de la peste, y abrazar los que usen un <code>OutputStream</code>. La misma consideración hay que tener si guardamos XMLs en una base de datos relacional, por ejemplo. Nada de <code>VARCHAR</code>, <code>CLOB</code> o similares; debemos usar un <code>BLOB</code>. 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 <code>byte[]</code>, y nunca como <code>String</code>.</p>
<p>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</p>
<pre class="prettyprint"><code>
<mensaje>Esto es un mensaje corto</mensaje>
</code></pre>
<p>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 <span lang="en">encoding</span> usado. Por ejemplo:</p>
<pre class="prettyprint"><code>
public byte[] generarXml(String texto) throws UnsupportedEncodingException {
return ("<mensaje>" + texto + "</mensaje>").getBytes("UTF-8");
}
</code></pre>
<p>puesto que como ya he comentado, si no se incluye <span lang="en">prolog</span>, la codificación por defecto es UTF-8 (y sí, el código es mejorable, pero se trata sólo de un sencillo ejemplo).</p>
<p>El caso contrario, interpretar un XML, no importa lo simple que pueda ser, creo que siempre es preferible el uso de un <span lang="en">parser</span> en condiciones. Pensad que sólo para averiguar el <span lang="en">encoding</span>, hay que hacer una primera lectura para buscar el <span lang="en">prolog</span> y su atributo <code>encoding</code> (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 <a href="http://apuntesadeteran.blogspot.com.es/2013/01/es-un-dato-del-problema.html">dato del problema</a>).</p>
Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com3tag:blogger.com,1999:blog-4515078847768540286.post-32814155340127450722013-08-21T07:32:00.001+02:002013-08-21T07:32:56.831+02:00ZIP sin compresión<p>Crear un fichero ZIP en Java es bastante fácil. No tenemos más que usar las clases <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipOutputStream.html">ZipOutputStream</a></code> y <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipEntry.html">ZipEntry</a></code> del paquete <code>java.util.zip</code>. Cada fichero dentro del ZIP está delimitado por una <code>ZipEntry</code>, que debemos crear y añadir al objeto <code>ZipOutputStream</code>, mediante su método <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipOutputStream.html#putNextEntry(java.util.zip.ZipEntry)">putNextEntry(ZipEntry)</a></code>. Una vez añadida la <code>ZipEntry</code>, se escribe en el <code>ZipOutputStream</code> los datos deseados, y finalmente se cierra la entrada con una llamada al método <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipOutputStream.html#closeEntry()">closeEntry()</a></code> del <code>ZipOutputStream</code>. Repetimos el proceso tantas veces como entradas tengamos que añadir, y terminamos con la inevitable llamada al <code>close()</code> del <code>ZipOutputStream</code>.</p>
<p>Veamos un ejemplo muy sencillo:</p>
<pre class="prettyprint"><code>
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class SimpleExample {
public static void main(String[] args) throws IOException {
File file = new File("example.zip");
ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(file));
addEntry(zipOutputStream, "example1.txt", "Some text".getBytes());
addEntry(zipOutputStream, "example2.txt", "More text".getBytes());
zipOutputStream.close();
}
private static void addEntry(ZipOutputStream zipOutputStream, String entryName, byte[] entryData) throws IOException {
ZipEntry zipEntry = new ZipEntry(entryName);
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(entryData);
zipOutputStream.closeEntry();
}
}
</code></pre>
<p>Ambas clases tienen un método <code>setMethod(int)</code>, que indica el método de compresión para los datos. Podemos pasar como argumento <code>ZipEntry.DEFLATED</code> (usar compresión) o <code>ZipEntry.STORED</code> (no usar compresión). Si usamos el <code>setMethod(int)</code> de <code>ZipOutputStream</code>, afectará a todas las entradas subsiguientes, mientras que si utilizamos el <code>setMethod(int)</code> de <code>ZipEntry</code>, afectará únicamente a esa entrada.</p>
<p>Si por el motivo que sea necesitamos entradas que no estén comprimidas (bien algunas en concreto, o todas las del ZIP), debemos llamar a <code>setMethod(int)</code> pasando como argumento <code>ZipEntry.STORED</code>.</p>
<p>¿Y ya está? Pues no, porque de ser así, no estaría escribiendo este <span lang="en">post</span>. Si en nuestro ejemplo anterior, simplemente modificamos el método <code>addEntry</code> de esta forma:</p>
<pre class="prettyprint"><code>
private static void addEntry(ZipOutputStream zipOutputStream, String entryName, byte[] entryData) throws IOException {
ZipEntry zipEntry = new ZipEntry(entryName);
zipEntry.setMethod(ZipEntry.STORED);
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(entryData);
zipOutputStream.closeEntry();
}
</code></pre>
<p>nos encontraremos con la siguiente y desagradable sorpresa:</p>
<pre class="prettyprint"><code>
Exception in thread "main" java.util.zip.ZipException: STORED entry missing size, compressed size, or crc-32
at java.util.zip.ZipOutputStream.putNextEntry(ZipOutputStream.java:167)
at SimpleExample.addEntry(SimpleExample.java:24)
at SimpleExample.main(SimpleExample.java:15)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Process finished with exit code 1
</code></pre>
<p>¿Por qué ocurre esto? Bueno, la clase <code>ZipEntry</code> tiene una serie de atributos que se ajustan automáticamente cuando se usa el método <code>DEFLATED</code>, pero no al usar <code>STORED</code>. Por alguna decisión de diseño que no comprendo, y que <b>no se avisa</b> en ningún sitio del API, al usar <code>STORED</code> hay que ajustar manualmente dichos atributos, llamando a los métodos <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipEntry.html#setSize(long)">setSize(long)</a></code>, <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipEntry.html#setCompressedSize(long)">setCompressedSize(long)</a></code> y <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/ZipEntry.html#setCrc(long)">setCrc(long)</a></code>.</p>
<p>¿Qué valores debemos usar? Para los dos primeros es inmediato: el número de bytes de los datos que queremos escribir (ya que al no usar compresión, <code>size</code> y <code>compressedSize</code>, coinciden). Si usamos un array de bytes, basta con leer su propiedad <code>length</code>. Para el último, hay que calcular el <code>CRC-32</code> de los datos, usando la clase <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/CRC32.html">CRC32</a></code> (que está en el mismo paquete <code>java.util.zip</code>). Así, la versión modificada de nuestro método de ejemplo <code>addEntry</code>, quedaría así:</p>
<pre class="prettyprint"><code>
private static void addEntry(ZipOutputStream zipOutputStream, String entryName, byte[] entryData) throws IOException {
ZipEntry zipEntry = new ZipEntry(entryName);
zipEntry.setMethod(ZipEntry.STORED);
zipEntry.setSize(entryData.length);
zipEntry.setCompressedSize(entryData.length);
CRC32 crc = new CRC32();
crc.update(entryData);
zipEntry.setCrc(crc.getValue());
zipOutputStream.putNextEntry(zipEntry);
zipOutputStream.write(entryData);
zipOutputStream.closeEntry();
}
</code></pre>
<p>No es demasiado complicado, pero hemos tenido que añadir 6 líneas (en vez de sólo una), y calcular nosotros el CRC de los datos. Además, si en vez de tener los datos a escribir en un array de bytes, lo tenemos que leer de un <code>InputStream</code>, la cosa se complica un poco, ya que para conocer el número de bytes y calcular el CRC, debemos recorrer el stream, y posteriormente deberemos recorrerlo nuevamente para escribirlo en el <code>ZipOutputStream</code>. Y si el <code>InputStream</code> es de un solo uso, tendríamos que ir copiando los bytes en algún otro lugar durante la primera pasada, para pasárselos luego al <code>ZipOutputStream</code>.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-49734870530866595972013-06-13T22:52:00.000+02:002013-06-17T12:44:51.800+02:00Abordando problemas de encoding, en Java<p>Posiblemente, los problemas derivados de un mal uso o mala comprensión del <span lang="en">encoding</span>, 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 <span lang="en">encoding</span>. 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 <a href="http://kunststube.net/encoding/">What Every Programmer Absolutely, Positively Needs To Know About Encodings And Character Sets To Work With Text</a>. 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 <span lang="en">encoding</span>»).</p>
<p>Para los que no puedan leer con fluidez la lengua de Shakespeare, resumiré los puntos más importantes:</p>
<ol>
<li>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.</li>
<li>Un <span lang="en">charset</span> o juego de caracteres, es un conjunto de caracteres representables, con un código numérico asociado a cada uno. Un <span lang="en">encoding</span> o codificación, es la forma de representar en bytes, el código numérico del carácter.</li>
<li>Unicode no es un <span lang="en">encoding</span>, es un <span lang="en">charset</span>. Uno muy interesante, ya que incluye todos los posibles caracteres de todos los idiomas del mundo. UTF-8 y UTF-16 son <span lang="en">encodings</span> de Unicode. Esta distinción es irrelevante en codificaciones donde no hay más de 256 caracteres, como ASCII o ISO-8859-1.</li>
<li>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 <span lang="en">encoding</span> cuando usamos estos caracteres.</li>
<li>En UTF-8, para caracteres con código mayor que 127 (hex <code class="lit">7F</code>), 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 «�» (<code class="prettyprint">U+FFFD</code>)</li>
</ol>
<p>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.</p>
<p>Veamos un ejemplo. La cadena <code class="prettyprint">"año"</code>, se representa en UTF-8 como <code class="lit">61 C3 B1 6F</code>. Es importante recordar que la <code class="prettyprint">'ñ'</code> se representa con dos bytes: <code class="lit">C3 B1</code> ¿Qué ocurre si una aplicación interpreta esta secuencia de bytes como un texto codificado en CP-1252? En esta codificación, el byte <code class="lit">C3</code> corresponde al caracter <code class="prettyprint">'Ã'</code> y el byte <code class="lit">B1</code> al caracter <code class="prettyprint">'±'</code>, por lo que esa aplicación mostrará <code class="prettyprint">"año"</code>.</p>
<p>Vamos con el ejemplo contrario. La cadena <code class="prettyprint">"año"</code> se representa en CP-1252 como <code class="lit">61 F1 6F</code>. Si una aplicación intenta interpretar esta secuencia de bytes como si fuera UTF-8, se encontrará con que el byte <code class="lit">F1</code> (<code class="lit">11110001</code>) es el primer byte de una secuencia de 4, pero que el siguiente byte debería estar entre <code class="lit">80</code> y <code class="lit">BF</code> (cumplir el patrón <code class="lit">10xxxxxx</code>) y no es así. Al ser <code class="lit">F1</code> un carácter «conflictivo», lo escapará con un <code class="prettyprint">'�'</code>, continuando con la intrepretación. De esta forma, se mostrará <code class="prettyprint">"a�o"</code>.</p>
<p>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 <code class="prettyprint">"año"</code> codificada en UTF-8, pero interpretada como ISO-8859-15. Como desde el punto de vista de una máquina, la cadena <code class="prettyprint">"año"</code> 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 <code class="prettyprint">"año"</code>: El motivo es que <code class="prettyprint">'Ã'</code> se representa en UTF-8 como <code class="lit">c3 83</code>, y <code class="prettyprint">'±'</code> como <code class="lit">c2 b1</code>. En CP-1252, el byte <code class="lit">c3</code> corresponde a <code class="prettyprint">'Ã'</code>, el <code class="lit">83</code> a <code class="prettyprint">'ƒ'</code>, el <code class="lit">c2</code> a <code class="prettyprint">'Â'</code> y el <code class="lit">b1</code> a <code class="prettyprint">'±'</code>.</p>
<p>Con esto ya tenemos unas de reglas de andar por casa, para averiguar lo que está pasando cuando vemos textos raros:</p>
<ol>
<li>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.</li>
<li>Si vemos que las eñes o vocales acentuadas son sustituidas por <b>dos</b> caracteres "extraños", la aplicación está interpretando como CP-1252 o ISO-8859 un texto que en realidad está codificado como UTF-8.</li>
<li>En general, si vemos que las eñes o vocales acentuadas son sustituidas por <b>2n</b> caracteres "extraños", se está cometiendo el error anterior <b>n</b> veces a lo largo del ciclo de vida del texto.</li>
</ol>
<p>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: <code class="prettyprint">'€'</code>. En CP-1252 se codifica como <code class="lit">80</code>, que en ISO-8859-1 e ISO-8859-15 corresponden a un caracter de control no imprimible. En ISO-8859-15 se codifica como <code class="lit">A4</code>, que en CP-1252 y en ISO-8859-1 corresponde al caracter <code class="prettyprint">'¤'</code>. 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 <code class="lit">E2 82 AC</code>. Es decir, con 3 bytes, por lo que habrá que tenerlo en cuenta al aplicar las reglas mencionadas.</p>
<p>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.</p>
<p>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 <code><a href="http://docs.oracle.com/javase/6/docs/api/java/lang/String.html">String</a></code> es como un array de caracteres, es decir, de <code class="prettyprint">char</code>. Salvo algunos métodos concretos, se opera a nivel de caracter. Así el resultado de</p>
<pre class="prettyprint"><code>
"1€ al año".length()
</code></pre>
<p>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).</p>
<p>Siguiente paso: los fuentes. Un fichero <code>.java</code> no deja de ser un simple fichero de texto plano. Es el compilador el que leerá este fichero y generará el <code>.class</code> 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:</p>
<pre class="prettyprint"><code>
public class SillyExample {
public static void main(String[] args) {
System.out.println("El niño se gastó 1 € en chuches.");
}
}
</code></pre>
<p>debería compilar y ejecutarse sin problemas, y mostrar en la consola lo mismo, sin importar la codificación de la plataforma:</p>
<pre><code>
El niño se gastó 1 € en chuches.
Process finished with exit code 0
</code></pre>
<p>Este comportamiento puede ser modificado, con la opción <code>-encoding</code> del compilador, que nos permite especificar la codificación de los fuentes. Obviamente, si la codificación de los ficheros <code>.java</code> 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.</p>
<p>Un primer paso para diagnósticar un problema de <span lang="en">encoding</span> con literales que se declaran en los fuentes, es averiguar la codificación de éstos. Para ello, lo más seguro es abrir un <code>.java</code> que sepamos que tiene algún carácter no ASCII, con un visor hexadecimal. Si el caracter está representado con dos bytes, el <code>.java</code> está en UTF-8. Si está representado con un solo byte, no.</p>
<p>No todas las cadenas que mostrará nuestra aplicación estarán en los <code>.java</code>. De hecho, eso no es una buena práctica. Lo habitual es tener los textos que se mostrarán al usuario en ficheros separados.</p>
<p>La forma habitual de mantener los textos que se mostrarán al usuario es mediante los ficheros <code>.properties</code>, 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 <code>.properties</code>, <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/ResourceBundle.html">ResourceBundle</a></code>, 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 <code class="prettyprint">\uXXXX</code>, donde <code>XXXX</code> es el código Unicode del caracter. Así, podríamos tener un <code>.properties</code> con la siguiente línea:</p>
<pre class="prettyprint"><code>
text=El niño se gastó 1 \u20AC en chuches.
</code></pre>
<p>Si queremos que nuestros <code>.properties</code> estén en otra codificación, como UTF-8, podríamos usar <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/PropertyResourceBundle.html">PropertyResourceBundle</a></code>, ya que tiene un constructor público al que se le pasa un <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/Reader.html">Reader</a></code>, pero perderíamos las facilidades de <code>ResourceBundle</code> para la internacionalización. Otra alternativa es buscar funcionalidades similares en otras librerías o <span lang="en">frameworks</span>.</p>
<p>Ya que mencionamos la clase <code>Reader</code>, cuando leemos o escribimos un fichero con <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/FileReader.html">FileReader</a></code> y <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/FileWriter.html">FileWriter</a></code>, 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 <code>.properties</code> leídos por <code>ResorceBundle</code>), como en el constructor <code><a href="http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#String(byte[])">String(byte[])</a></code> y el método <code><a href="http://docs.oracle.com/javase/6/docs/api/java/lang/String.html#getBytes()">byte[] getBytes()</a></code> de <code>String</code>, o las clases <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/InputStreamReader.html">InputStreamReader</a></code> y <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/OutputStreamWriter.html">OutputStreamWriter</a></code>. 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.</p>
<p>Se puede modificar la codificación por defecto con la variable de sistema <code>file.encoding</code>, pero sólo podemos hacerlo en el arranque de la VM, con la opción <code>-D</code> (por ejemplo, <code>-Dfile.encoding=UTF-8</code>). Si lo hacemos en el código, usando <code class="prettyprint">System.setProperty("file.encoding", "UTF-8")</code>, no funcionará. En cualquier caso, insisto en que esa configuración no afecta al comportamiento de <code>ResourceBundle</code>, que seguirá esperando los <code>.properties</code> en ISO-8859-1.</p>
<p>Así que aquí tenemos dos puntos más a revisar cuando encontramos problemas de <span lang="en">encoding</span> 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 <span lang="en">breakpoint</span> evaluamos con un buen IDE el contenido de un <code>String</code>, podremos ver el número de caracteres, o mejor aun, el array de <code class="prettyprint">char</code> interno. Si vemos los caracteres que deben donde deben, el problema no está en la lectura de ficheros u otros recursos.</p>
<p>Para ayudarnos en esta tarea, es muy útil la web <a href="http://www.fileformat.info/info/unicode/char/search.htm">FileFormat.Info</a>, 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 «<a href="http://www.fileformat.info/info/unicode/char/f1/index.htm">ñ</a>».</p>
<p>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.</p>
<p>Para determinar la codificación que debe emplear un navegador a la hora de mostrar el HTML de una <span lang="en">response</span> HTTP, se utiliza la cabecera HTTP <code>Content-Type</code>. 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 <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletResponse.html#setContentType(java.lang.String)">setContentType(String)</a></code> o <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletResponse.html#setCharacterEncoding(java.lang.String)">setCharacterEncoding(String)</a></code> del objeto <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletResponse.html">ServletResponse</a></code> (no son exactamente equivalentes, y hay peculiaridades que conviene consultar en la documentación), o mediante
el atributo <code>contentType</code> de la directriz JSP <code>page</code>. Por ejemplo:</p>
<pre class="prettyprint"><code>
<%@ page contentType="text/html;charset=UTF-8"%>
</code></pre>
<p>Si esta cabecera no aparece, o no indica la codificación, el navegador busca una etiqueta <code>meta</code> presente en el HTML de la página, que a su vez puede ponerse como</p>
<pre class="prettyprint"><code>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
</code></pre>
<p>o como</p>
<pre class="prettyprint"><code>
<meta charset='utf-8'>
</code></pre>
<p>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 <code>&euro;</code> 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.</p>
<p>Aunque se puede especificar la codificación del <span lang="en">response</span> sólo con los métodos de <code>ServletResponse</code>, si usamos JSPs debemos especificar la codificación también en la directiva <code>page</code>. Recordemos que una JSP se traduce internamente en un <code>.java</code> que luego se compila. La implementación concreta depende del servidor utilizado, pero puede ocurrir (y de hecho ocurre con Tomcat 6) que el <span lang="en">encoding</span> utilizado para generar el <code>.java</code> 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 <code>file.encoding</code>).</p>
<p>Otra fuente de error son los formularios. Cuando se hace <span lang="en">submit</span> de un formulario, el navegador utiliza en la <span lang="en">request</span> la misma codificación que haya usado para mostrar la página. Este comportamiento se puede modificar mediante el atributo <code>accept-charset</code> de la etiqueta <code>form</code>, indicando el encoding a usar (sólo el <span lang="en">encoding</span>, no todo el <code>Content-Type</code>). Sin embargo, no funciona en todos los navegadores, por lo que no es conveniente utilizarlo.</p>
<p>En el lado del servidor, la clase <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html">ServletRequest</a></code> tiene un método <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#getCharacterEncoding()">getCharacterEncoding()</a></code>, que debería devolvernos la codificación de la <span lang="en">request</span>. ¿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 <code>null</code>. Es decir, no podemos confiar en dicho método para saber la codificación de la <span lang="en">request</span>. Así que debemos usar una de forma consistente en el <span lang="en">response</span>, y asumir que las <span lang="en">request</span> nos llegan con esa misma codificación.</p>
<p>Hay una gran excepción a esta regla (<span lang="en">encoding</span> de la <span lang="en">request</span> = <span lang="en">encoding</span> de la página del formulario), que sucede cuando usamos Ajax. Ajax usa por defecto UTF-8 en la <span lang="en">request</span>. Si no tenemos esto en cuenta, en una aplicación web en la que se use ISO-8859-15, por ejemplo, recibiremos una <span lang="en">request</span> UTF-8, que el servidor interpretará incorrectamente.</p>
<p>Una solución es especificar de forma explícita en nuestra aplicación, que la <span lang="en">request</span> viene codificada como UTF-8. Esto se puede hacer con el método <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#setCharacterEncoding(java.lang.String)">setCharacterEncoding(String)</a></code> de la clase <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html">ServletRequest</a></code>. Este método simplemente establece con qué codificación se debe leer la <span lang="en">request</span>. Es decir, no realiza ninguna transformación de <span lang="en">encoding</span>. Si la <span lang="en">request</span> 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 <code>setCharacterEncoding(String)</code> antes de leer algún parámetro de la <span lang="en">request</span>.</p>
<p>Una aplicación web que use una codificación en unas <span lang="en">request</span>, y otra diferente en otras, es muy mala idea. También es mala idea usar codificaciones diferentes para la <span lang="en">request</span> y el <span lang="en">response</span>. 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 <span lang="en">request</span> y <span lang="en">response</span> en un filtro (una clase que implemente <code><a href="http://docs.oracle.com/javaee/6/api/javax/servlet/Filter.html">Filter</a></code>) y configurarlo en el <code>web.xml</code>. Como en las JSPs que usemos, debemos especificar en la directiva <code>page</code> 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).</p>
<p>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 <span lang="en">request</span>, evitamos tener que complicarnos con ese tema. El problema son los <code>.properties</code>, que si los vamos a leer con <code>ResourceBundle</code>, 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 <span lang="en">framework</span>, buscar si incluye algún sistema alternativo que soporte UTF-8. También podemos optar por usar la clase <code>PropertyResourceBundle</code>, que nos permite usar un <code>Reader</code>, de forma que tengamos control de la codificación a usar, y si necesitamos multilenguaje, implementar nosotros mismos el comportamiento de <code>ResourceBundle</code> en ese caso (que básicamente es añadir al nombre el <code>Locale</code> utilizado, antes de la extensión, e ir eliminando fragmentos del mismo hasta encontrar el recurso).</p>
Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com32tag:blogger.com,1999:blog-4515078847768540286.post-59340017026450457372013-03-12T22:51:00.000+01:002013-06-13T22:35:00.607+02:00Comprimir con gzip una request SOAP, en Spring Web Services<p>En un proyecto reciente, tuve que implementar un cliente SOAP que debía ineractuar con un servicio web de un tercero, ya implementado y definido, esto es, el descriptor del servicio y la forma de interactuar con el mismo eran <a href="http://apuntesadeteran.blogspot.com.es/2013/01/es-un-dato-del-problema.html">un dato del problema</a>. Uno de estos requisitos era que la request HTTP tenía que ir comprimida con gzip. Si no era así, el servidor devolvería un error (en este caso, era una exigencia razonable, pues los datos a enviar podían pesar bastante).
</p>
<p>De entre las opciones que tenemos por ahí, me decanté por emplear <a href="http://www.springsource.org/spring-web-services">Spring Web Services</a>, que encuentro cómodo y sencillo de usar. Los que tengáis algo de experiencia con este <span lang="en">framework</span>, sabréis que para <a href="http://static.springsource.org/spring-ws/sites/2.0/reference/html/client.html">implementar un cliente</a>, debemos recurrir a la clase <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/client/core/WebServiceTemplate.html">WebServiceTemplate</a></code>. Internamente, esta clase utiliza una implementación de <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/WebServiceMessageSender.html">WebServiceMessageSender</a></code> para el transporte. Si no se configura, la implementación por defecto es <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnectionMessageSender.html">HttpUrlConnectionMessageSender</a></code>, que utiliza por debajo las clases del propio JRE para el transporte HTTP. Otra opción puede ser utilizar <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpComponentsMessageSender.html">HttpComponentsMessageSender</a></code>, que utiliza la librería de Apache <a href="http://hc.apache.org/httpcomponents-client-ga/index.html">HttpClient</a>.
</p>
<p>Sin embargo, ninguna de estas implementaciones nos ofrece la posibilidad de comprimir la <span lang="en">request</span>. El <span lang="en">response</span> puede venir comprimido sin problemas, sin más que establecer el <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/AbstractHttpWebServiceMessageSender.html#setAcceptGzipEncoding(boolean)">setAcceptGzipEncoding(boolean)</a></code> a <code>true</code> (cosa que ya viene así por defecto), pero no tenemos ayuda para la <span lang="en">request</span>. Buscando por la red, uno tampoco encuentra mucha información, salvo indicaciones de que debemos implementar nuestro propio <code>WebServiceMessageSender</code>. Y efectivamente, así es. Pero no os preocupéis, no es necesario implementar uno desde cero.
</p>
<p>Si miramos el código fuente de <code>HttpUrlConnectionMessageSender</code>, veremos que la implementación del método <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnectionMessageSender.html#prepareConnection(java.net.HttpURLConnection)">createConnection(URI)</a></code> es algo así (pongo el de la versión 2.0.5):
</p>
<pre class="prettyprint"><code>
public WebServiceConnection createConnection(URI uri) throws IOException {
URL url = uri.toURL();
URLConnection connection = url.openConnection();
if (!(connection instanceof HttpURLConnection)) {
throw new HttpTransportException("URI [" + uri + "] is not an HTTP URL");
}
else {
HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
prepareConnection(httpURLConnection);
return new HttpUrlConnection(httpURLConnection);
}
}
</code></pre>
<p>Esto es, a partir del <code><a href="http://docs.oracle.com/javase/6/docs/api/java/net/URI.html">URI</a></code> pasado, obtiene un objeto <code><a href="http://docs.oracle.com/javase/6/docs/api/java/net/URL.html">URL</a></code>, que utiliza a su vez para obtener una <code><a href="http://docs.oracle.com/javase/6/docs/api/java/net/URLConnection.html">URLConnection</a></code>. Como el protocolo es HTTP, sabemos que en realidad, el objeto es una instancia de <code><a href="http://docs.oracle.com/javase/6/docs/api/java/net/HttpURLConnection.html">HttpURLConnection</a></code>. Finalmente, envuelve este objeto con una <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnection.html">HttpUrlConnection</a></code>, que es lo que devuelve. Mucho ojito con los nombres, que es fácil liarse. <code>HttpURLConnection</code> es la clase del JDK que hereda de <code>URLConnection</code>, mientras que <code>HttpUrlConnection</code> es la clase de Spring que implementa <code>WebServiceConnection</code>.</p>
<p>El objeto <code>WebServiceConnection</code> es lo que se va a utilizar dentro del <code>WebServiceMessageSender</code> para tratar con la conexión HTTP, por lo que también tendremos que hacernos una implementación propia. Vemos que <code>HttpUrlConnection</code> (la de Spring) tiene un método <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnection.html#getRequestOutputStream()">getRequestOutputStream()</a></code> que devuelve el <code>OutputStream</code> donde escribir lo que queramos enviar con la <span lang="en">request</span>. Lo que necesitamos es envolver este <code>OutputStream</code> con un <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/GZIPOutputStream.html">GZIPOutputStream</a></code> (aunque <code>HttpUrlConnection</code> también tiene un método <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnection.html#getConnection()">getConnection</a></code> que devuelve la <code>HttpURLConnection</code> original del JDK, no es usado en el <code>HttpUrlConnectionMessageSender</code>).
</p>
<p>Así que lo que hacemos es crearnos una clase que herede de <code>HttpUrlConnection</code>, y sobreescribimos su método getRequestOutputStream(). Para que el método pueda ser llamado más de una vez y devuelva el mismo objeto, utilizaremos una variable de instancia donde almacenar dicho objeto:</p>
<pre class="prettyprint"><code>
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.zip.GZIPOutputStream;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.http.HttpUrlConnection;
public class GZipHttpUrlConnection extends HttpUrlConnection {
private OutputStream outputStream;
GZipHttpUrlConnection(HttpURLConnection connection) {
super(connection);
}
@Override
protected OutputStream getRequestOutputStream() throws IOException {
if (outputStream == null) {
outputStream = new GZIPOutputStream(super.getRequestOutputStream());
}
return outputStream;
}
}
</code></pre>
<p>Esta clase es la que devolverá nuestra implementación de <code>WebServiceMessageSender</code> en el método <code>createConnection(URI)</code>. Para ello, heredaremos de <code>HttpUrlConnectionMessageSender</code> y sobreescribiremos el método <code>createConnection(URI)</code>:
</p>
<pre class="prettyprint"><code>
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.http.HttpTransportException;
import org.springframework.ws.transport.http.HttpUrlConnectionMessageSender;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLConnection;
class GZipHttpUrlConnectionMessageSender extends HttpUrlConnectionMessageSender {
@Override
public WebServiceConnection createConnection(URI uri) throws IOException {
URLConnection connection = uri.toURL().openConnection();
if (!(connection instanceof HttpURLConnection)) {
throw new HttpTransportException("URI [" + uri + "] is not an HTTP URL");
}
HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
prepareConnection(httpURLConnection);
return new GZipHttpUrlConnection(httpURLConnection);
}
}
</code></pre>
<p>Aún no hemos terminado. El código anterior, tal y como lo he puesto, no funcionará. Hay que hacer dos ajustes imprescindibles.
</p>
<p>El primero es añadir la cabecera HTTP <code>Content-Encoding</code> con el valor <code>gzip</code>, mediante el método <code><a href="http://docs.oracle.com/javase/6/docs/api/java/net/URLConnection.html#setRequestProperty(java.lang.String, java.lang.String)">setRequestProperty(String, String)</a></code> de <code>URLConnection</code>. Si no, el servidor no sabrá que la <span lang="en">request</span> viene comprimida. Podemos hacerlo en el constructor de <code>GZipHttpUrlConnection</code>:</p>
<pre class="prettyprint"><code>
GZipHttpUrlConnection(HttpURLConnection connection) {
super(connection);
connection.setRequestProperty("Content-Encoding", "gzip");
}
</code></pre>
<p>O podemos sobreescribir el método <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnectionMessageSender.html#prepareConnection(java.net.HttpURLConnection)">prepareConnection(HttpURLConnection)</a></code> en <code>GZipHttpUrlConnectionMessageSender</code>:
</p>
<pre class="prettyprint"><code>
@Override
protected void prepareConnection(HttpURLConnection connection) throws IOException {
super(connection);
connection.setRequestProperty("Content-Encoding", "gzip");
}
</code></pre>
<p>La elección es vuestra.</p>
<p>El segundo ajuste tiene que ver con el <a href="http://apuntesadeteran.blogspot.com.es/2013/02/el-metodo-flush-de-gzipoutputstream-no.html">anterior <span lang="en">post</span></a>. Resulta que ninguna clase involucrada del framework Spring Web Services está cerrando el <code>OutputStream</code> de la <span lang="en">request</span>. Así, nos encontraremos con la desagradable sorpresa de que al servidor sólo le llegan los 10 bytes de la cabecera gzip. Para evitarlo, debemos cerrar nosotros mismos el <code>OutputStream</code>. ¿Donde? Pues un buen sitio donde hacerlo es sobreescribiendo el método <code><a href="http://static.springsource.org/spring-ws/sites/2.0/apidocs/org/springframework/ws/transport/http/HttpUrlConnection.html#onSendAfterWrite(org.springframework.ws.WebServiceMessage)">onSendAfterWrite(WebServiceMessage)</a></code> de <code>HttpUrlConnection</code>, ya que como se indica en la documentación, se invoca tras finalizar la escritura del mensaje. Por tanto, nuestra clase <code>GZipHttpUrlConnection</code> quedaría así (he añadido también el ajuste del <code>Content-Encoding</code>):
</p>
<pre class="prettyprint"><code>
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.zip.GZIPOutputStream;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.http.HttpUrlConnection;
class GZipHttpUrlConnection extends HttpUrlConnection {
private OutputStream outputStream;
GZipHttpUrlConnection(HttpURLConnection connection) {
super(connection);
connection.setRequestProperty("Content-Encoding", "gzip");
}
@Override
protected OutputStream getRequestOutputStream() throws IOException {
if (outputStream == null) {
outputStream = new GZIPOutputStream(super.getRequestOutputStream());
}
return outputStream;
}
@Override
protected void onSendAfterWrite(WebServiceMessage message) throws IOException {
if (outputStream != null) {
outputStream.close();
}
super.onSendAfterWrite(message);
}
}
</code></pre>
<p>Aclararé que es necesario llamar al <code>onSendAfterWrite(WebServiceMessage)</code> del padre ya que, aunque la documentación diga que no, sí que tiene una implementación (hace un <code>connection.connect()</code>). Y además, hay que hacerlo al final, o si no, nos saltará una excepción al hacer el <code>close()</code>.
</p>
<p>Bien, con esto ya lo tenemos todo. Ya sólo nos queda configurar adecuadamente nuestra instancia de <code>WebServiceTemplate</code> en Spring, para que reciba una instancia de <code>GZipHttpUrlConnectionMessageSender</code> como <code>messageSender</code>. Si lo hacemos mediante XML, añadiríamos algo así:
</p>
<pre class="prettyprint"><code>
<bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
<property name="messageSender">
<bean class="GZipHttpUrlConnectionMessageSender"/>
</property>
</bean>
</code></pre>
<p>Y con esto, nuestro cliente SOAP ya tendría capacidad para enviar la <span lang="en">request</span> comprimida con gzip.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0tag:blogger.com,1999:blog-4515078847768540286.post-16448600708860987642013-02-28T23:05:00.001+01:002013-03-08T14:31:23.553+01:00El método flush() de GZIPOutputStream, no garantiza el flush de los datos<p>La clase <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/GZIPOutputStream.html">java.util.zip.GZIPOutputStream</a></code> permite comprimir un conjunto de bytes en el formato gzip. Esta clase hereda de <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/FilterOutputStream.html">java.io.FilterOutputStream</a></code>, por lo que su uso es muy sencillo: envolvemos un <code><a href="http://docs.oracle.com/javase/6/docs/api/java/io/OutputStream.html">java.io.OutputStream</a></code> cualquiera con la clase mencionada, y vamos llamando a las distintas variantes de <code>write()</code>. Al terminar el proceso (llamando a <code>close()</code>), habremos escrito en el stream envuelto, los datos comprimidos.</p>
<p>Pero hay un detalle que no está reflejado en la documentación del JDK, y es que <code>GZIPOutputStream</code> rompe el contrato implícito de sus padres (al menos, en Java 6). Si llamamos al método <code>flush()</code> de <code>GZIPOutputStream</code>, no siempre forzaremos el volcado de datos. Este comportamiento no es en realidad consecuencia de una reimplementación de <code>flush()</code> en <code>GZIPOutputStream</code>. Si observamos la documentación, comprobaremos que <code>flush()</code> es directamente heredado de <code>FilterOutputStream</code>, y lo que hace es llamar al método <code>flush()</code> del stream subyacente.</p>
<p>El problema está en realidad en que la clase padre de <code>GZIPOutputStream</code>, <code><a href="http://docs.oracle.com/javase/6/docs/api/java/util/zip/DeflaterOutputStream.html">DeflaterOutputStream</a></code>, sobreescribe los métodos <code>write()</code>. Esta clase padre es la que realiza la tarea de compresión de datos, ya que <code>GZIPOutputStream</code> simplemente añade una cabecera y una cola. Las llamadas a las distintas variantes de <code>write()</code>, no siempre se traducen en llamadas a los métodos <code>write()</code> del stream subyacente. <code>DeflaterOutputStream</code> utiliza un buffer propio donde guarda los bytes resultantes de la compresión, y sólo los pasa al stream subyacente en algunos casos. Esto es debido al algoritmo de compresión, que se basa en la búsqueda de símbolos y secuencias repetidas.</p>
<p>Sólo una llamada a los métodos <code>close()</code> o <code>finish()</code> nos garantiza que el stream subyacente ha recibido la totalidad de los bytes. Pero al hacerlo, cerramos el stream a nuevos datos. El método <code>finish()</code> es exclusivo de <code>DeflaterOutputStream</code> y fuerza la compresión, escribiendo en el stream subyacente. Una vez hecho esto, cualquier nueva llamada a <code>write()</code> lanzará una excepción. El método <code>close()</code>, llama al propio <code>finish()</code> y al <code>close()</code> del stream subyacente.</p>
<p>Supongo que algunos pensaréis: «Bueno, no pasa nada. Después de todo, al terminar de usar un stream, hay que cerrarlo». Sí, pero este comportamiento no cumple el <a href="http://es.wikipedia.org/wiki/Principio_de_sustituci%C3%B3n_de_Liskov">principio de sustitución de Liskov</a>. Esto puede causar problemas (y de hecho los causa, como veremos en el siguiente post) cuando pasamos nuestro objeto <code>GZIPOutputStream</code> a una librería o a un framework, y no tenemos control de lo que se hace con él.</p>
<p>Un síntoma de este problema es encontrarnos con la desagradable sorpresa de que tras llamar varias veces a los métodos <code>write()</code>, al stream subyacente sólo llegan los 10 bytes de la cabecera gzip, por mucho <code>flush()</code> que hagamos. Si esto ocurre, tened la seguridad de que nadie está haciendo un <code>close()</code> del objeto <code>GZIPOutputStream</code>.</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com1tag:blogger.com,1999:blog-4515078847768540286.post-22754024749533051952013-02-20T14:52:00.000+01:002013-03-08T14:30:36.599+01:00Es un dato del problema<p>He decidido empezar este blog con una pequeña anécdota, ya que muchas de las soluciones que veáis por aquí, lo que hacen en realidad es sortear un problema de diseño en el software, o forzar una herramienta para que pueda ser utilizada en un caso de uso nada recomendable. O puede que cosas peores, que os hagan llevar las manos a la cabeza y decir «es que aquí hay un problema de base; habría que rehacerlo todo». Bueno, bienvenidos al mundo real. En ocasiones tenemos que tratar con código heredado que nadie quiere tirar, con imposiciones técnicas por decisiones no técnicas, o simplemente con una fecha tan ajustada que sólo da tiempo a parchear algo de mala manera. Es lo que yo llamo «un dato del problema».</p>
<p>En realidad, el término no es creación mía sino que viene del jefe de proyecto que tuve en mi primer trabajo. Corría el año 1998, y debíamos realizar varias aplicaciones web para un nuevo operador de telefonía. A saber, una intranet para su personal, una extranet para sus distribuidores, y la web corporativa. Las aplicaciones debían correr sobre Java, pero además había una condición por parte del cliente, que nos resultó extraña: El desarrollo debía realizarse con una herramienta llamada <a href="http://en.wikipedia.org/wiki/NetDynamics_Application_Server">NetDynamics</a>, recién adquirida por Sun Microsystems. Cuando preguntamos por qué, teniendo en cuenta la filosofía de Java «write once, run anywhere», y que debería dar igual qué IDE o plataforma usáramos, nuestro jefe de proyecto nos dijo «porque es un dato del problema». Ante nuestras caras de interrogación, nos hizo recordar cómo eran los problemas de matemáticas o física que nos ponían en el colegio. A veces, había partes del enunciado que no sabías muy bien para qué te servían, pero que tenías que meterlas como sea, porque eran un dato del problema. Y si no las usabas, seguro que el profesor te lo calificaba como incorrecto. Esto era lo mismo.</p>
<p>Nuestra experiencia no fue agradable. Pese a que el planteamiento era bueno (no existían en aquel entonces las JSPs, y con esta plataforma podíamos escribir páginas HTML con etiquetas especiales que renderizaban el valor de parámetros de la <span lang="en">request</span> o la sesión), la herramienta estaba inmadura, y encontramos algún bug grave. Como además éramos unos novatos imberbes, tardábamos muchísimo en identificar los bugs de NetDynamics como tales, pensando que era culpa nuestra por no saber usar la herramienta. El desarrollo se retrasó una barbaridad, trabajamos día y noche, se crisparon los ánimos... En fin, supongo que algunos habréis pasado por situaciones similares.</p>
<p>A lo largo de mi carrera profesional, me he encontrado con situaciones parecidas (aunque nunca con desenlaces tan desastrosos). Decisiones inamovibles que condicionan el desarrollo del proyecto, sin posibilidad de valorar otras opciones. Lo que desde entonces he llamado, «un dato del problema».</p>Alfonso de Terán Rivahttp://www.blogger.com/profile/00534038511753640060noreply@blogger.com0