Minientrada

Publicar aplicaciones en Tomcat mediante MAVEN.

La mayoría de las veces que desarrollo aplicaciones web para Tomcat necesito hacer pruebas rápidamente para comprobar que lo que estoy haciendo funciona correctamente y se ve como debe.

Para conseguir una velocidad óptima en estas pruebas, existe la opción de crear un Servidor en Eclipse y publicar la aplicación, así cada vez que haga un cambio en el código fuente, este se compila y despliega automáticamente en el servidor, pudiendo pasar rápidamente a la fase de pruebas.

Este método está muy bien y puede que sea la manera más rápida de probar, de hecho, suelo utilizarla mucho en las fases iniciales de los proyectos cuando todo es desarrollo nuevo y nada es mantenimiento. Desafortunadamente esta opción tiene también sus contras. La mayoría de las veces, los entornos de integración, de calidad o pre-productivos no tienen instaladas herramientas de desarrollo como Eclipse, incluso ni si quiera tienen instalados gestores de ventanas para así aligerar el consumo de RAM y de CPU.

Por esto, una de mis alternativas favoritas y dado que la mayor parte de las veces utilizo MAVEN para la gestión del ciclo de vida de las aplicaciones Java, es utilizar el plugin Maven de Tomcat.

Esta entrada no es más que un resúmen práctico para incluir este plugin en los desarrollos Java.

Requisitos para seguir el ejemplo.

Para reproducir el ejemplo y tenerlo operativo al 100% he utilizado las siguientes herramientas para hacerlo funcionar.

IDE desarrollo: Eclipse 3.6 (Neon).
Máquina virtual Java: JDK 1.8.0_65.
Servidor de aplicaciones: Apache Tomcat 8.5
Ciclo de vida: Maven 3.x (en mi caso la 3.3.9).

Es necesario haberse descargado el servidor de aplicaciones (contenedor JSP/Servlets en este caso) y poder acceder a la URL http://<dirección ip>:<puerto>/manager. También es necesaria una aplicación web a la que añadiremos todo lo necesario para probar el despliegue automático en dicho servidor.

Configurar tomcat para poder hacer despliegues vía script.

Para subir al servidor de Tomcat una aplicación, el plugin de maven utiliza por defecto la interfaz de administración http://servidor:puerto/manager/text. Para poder utilizar esta interfaz, el usuario de tomcat debe contar con el rol manager-script, si no la publicación no funcionará correctamente.

Para ello, en el fichero $CATALINA_BASE/conf/tomcat_users.xml hay que asignar este rol a un usuario existente, o crear un usuario para este tipo de despliegues con este rol.

Fichero: $CATALINA_BASE/conf/tomcat_users.xml

[...]
<user username="eduardo" password="xxxxxxx" roles="manager-gui,manager-script"/>
[...]

En este caso, el usuario eduardo tiene permisos para subir aplicaciones desde la interfaz web (manager-gui) y desde el plugin de Maven (manager-script).

Añadir el plugin de tomcat a la aplicación.

Añadir el plugin de tomcat en la aplicación se hace en el fichero pom.xml.

Fichero: pom.xml

    [...]
    <build>
        <plugins>
            [...]
            <!-- Tomcat plugin -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat7-maven-plugin</artifactId>
                <version>2.2</version>
                <configuration>
                    <url>http://localhost:8080/manager/text</url>
                    <username>username</username>
                    <password>password</password>
                </configuration>
            </plugin>
            [...]
        </plugins>
    </build>

El plugin tomcat7-maven-plugin pese al nombre funciona correctamente con Tomcat7, Tomcat8 y Tomcat85. No lo he probado con Tomcat9 debido a que todavía está en su versión Milestone en el momento de crear esta entrada.

La url para desplegar en Tomcat mediante script es http://servidor:puerto/manager/text.

El username y el password son el usuario y la contraseña de un usuario con el rol manager-script.

Por defecto, el plugin publica la aplicación con el nombre como contexto de aplicación. Se puede cambiar este contexto en la configuración del plugin añadiendo la entrada <path>/nombre-contexto</path>. Por ejemplo, imaginemos que se quiere diferenciar las publicaciones hechas en el entorno de integración de las hechas en el entorno de desarrollo, se añade <path>/xxxxx-integracion</path>; donde xxxxxx puede ser el nombre de la aplicación. De esta forma  ya podríamos acceder a la aplicación desde este nuevo contexto: http://localhost:8080/xxxxxx-integracion/.

Adicionalmente, el contenido de <path> podría incluirse en un fichero de propiedades para diferentes perfiles de maven y así conseguir que el pom.xml sea el mismo en todos los entornos y solo cambie la manera de invocarlo. Pero estas configuraciones más avanzadas de MAVEN no son el objeto de esta entrada y las dejaremos para otro momento.

Ejecutar el despliegue en Tomcat utilizando Maven.

Una vez que todo está configurado correctamente, es el momento de ejecutar el despliegue utilizando Maven.

Para ejecutarlo se lanza el siguiente comando:

   mvn org.apache.tomcat.maven:tomcat7-maven-plugin:2.2:redeploy   

La respuesta sería algo así:

[INFO]
[INFO] <<< tomcat7-maven-plugin:2.2:redeploy (default-cli) < package @ xxxxxx<<<
[INFO]
[INFO] --- tomcat7-maven-plugin:2.2:redeploy (default-cli) @ xxxxxx---
[INFO] Deploying war to http://localhost:8080/xxxxxx
Uploading: http://localhost:8080/manager/text/deploy?path=%2Fxxxxxx&update=true
Uploaded: http://localhost:8080/manager/text/deploy?path=%2Fxxxxxx&update=true (3118 KB at 1734.6 KB/sec)

[INFO] tomcatManager status code:200, ReasonPhrase:
[INFO] OK - Desplegada aplicación en trayectoria de contexto /xxxxxxx
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 8.679 s
[INFO] Finished at: 2016-09-02T12:36:59+02:00
[INFO] Final Memory: 15M/201M
[INFO] ------------------------------------------------------------------------

 Links

Página oficial del plugin de maven para Tomcat 7

 

Minientrada

JSF 2.x Hola Mundo

En esta entrada voy a explicaros como hacer una aplicación muy básica con JSF 2.x, un Hola Mundo. JSF (o Java Server Faces) es la tecnología estándar de Java para simplificar la interfaz de usuario de aplicaciones web.

La versión 2.0 de JSF fue lanzada en Agosto de 2009 y corresponde con Java EE 6. La versión 2.2 de JSF fue lanzada en Abril de 2013 y corresponde con Java EE 7.

Para probar la aplicación voy a publicarla en un contenedor de servlets Apache Tomcat 8.5. Al no ser un servidor de aplicaciones homologado con Java EE habrá que incluir algunas librerías que no serían necesarias si la publicación se hiciera en un contenedor de aplicaciones homologado.

Al tener que incluir las librerías en la aplicación Java voy a utilizar la versión 2.2 de JSF. Hay que tener en cuenta que si se va a publicar en un servidor de aplicaciones existente la versión de JSF que incorporan depende de la Java EE homologada que implementen; Java EE 5 – JSF 1.2, Java EE 6 – JSF 2.0, Java EE 7 – JSF 2.2.

Entorno utilizado para desarrollar el ejemplo.

Para que se pueda reproducir el ejemplo y esté operativo al 100% comento qué entorno he utilizado para hacerlo funcionar.

IDE desarrollo: Eclipse 3.6 (Neon).
Máquina virtual Java: JDK 1.8.0_65.
Servidor de aplicaciones: Apache Tomcat 8.5
Ciclo de vida: Maven 3.x
Tecnología: JSF 2.2 (Java EE 7)

Módulo web de aplicación.

Este ejemplo es tan sencillo que va a contar con un único módulo; el módulo web. El módulo web genera un WAR que se despliega directamente en Apache Tomcat.

Fichero: pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 <modelVersion>4.0.0</modelVersion>
 <groupId>es.egv.jee6.jsf2</groupId>
 <artifactId>BasicoJSF2</artifactId>
 <version>1.0.0</version>
 <packaging>war</packaging>

 <dependencies>
   <dependency>
     <groupId>com.sun.faces</groupId>
     <artifactId>jsf-api</artifactId>
     <version>2.2.11</version>
   </dependency>

   <dependency>
     <groupId>com.sun.faces</groupId>
     <artifactId>jsf-impl</artifactId>
     <version>2.2.11</version>
   </dependency>

   <dependency>
     <groupId>javax.el</groupId>
     <artifactId>javax.el-api</artifactId>
     <version>3.0.1-b04</version>
   </dependency>

   <dependency>
     <groupId>javax.servlet.jsp.jstl</groupId>
     <artifactId>jstl-api</artifactId>
     <version>1.2</version>
   </dependency>
 </dependencies>

</project>

En este caso, al desplegarse la aplicación en un contenedor de Servlets y no en un servidor de aplicaciones se incluyen como dependencias las librerías: jsf-api, jsf-impl (mojarra), javax.el-api y jstl-api. Si hubieramos desplegado esta aplicación en un servidor de aplicaciones homologado con la Java EE 6 o superiores, solo deberíamos incluir como dependencia la librería jsf-api, ya que el resto serían proporcionadas por el contenedor. En ese caso tener en cuenta también que la versión Java EE 6 incluye la versión 2.0 de JSF.

Fichero: web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
  <display-name>BasicoJSF2</display-name>
  <welcome-file-list>
    <welcome-file>holamundo.xhtml</welcome-file>
  </welcome-file-list>
  <servlet>
    <servlet-name>Faces Servlet</servlet-name>
    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
 
    <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.jsf</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.faces</url-pattern>
  </servlet-mapping>
  <servlet-mapping>
    <servlet-name>Faces Servlet</servlet-name>
    <url-pattern>*.xhtml</url-pattern>
  </servlet-mapping>

  <context-param>
    <param-name>javax.faces.PROJECT_STAGE</param-name>
    <param-value>Development</param-value>
  </context-param>
</web-app>

Destacar dos cosas importantes en el fichero de configuración del módulo web:

  • Al cargarse el módulo web en el servidor se ejecuta el servlet que JSF incorpora para interpretar sus páginas (javax.faces.webapp.FacesServlet).
  • Mientras se esta desarrollando la aplicación es conveniente informar el parámetro JSF javax.faces.PROJECT_STAGE con el valor Development. De esta forma el servidor proporciona mucha información de DEBUG que de otra manera no estaría disponible. Cuando se quiera poner la aplicación en producción se sustituye ese valor por el de Production.

JSF 2.x Managed Bean

Los managed bean son clases java que se registran en el framework de JSF, lo que permite que puedan ser consumidos desde las páginas dinámicas. Desde la versión 2.0 de JSF este registro se puede llevar a cabo mediante la anotación @javax.faces.bean.ManagedBean.

Los managed bean contienen métodos get y set para poder guardar y recoger datos de la sesión y métodos de lógica de negocio, aunque si la aplicación es compleja la lógica de negocio recomiendo que recaiga en componentes EJB.

Fichero: HolaMundoMBean.java

package es.egv.jee6.jsf2.mbean;

import java.io.Serializable;
import java.text.SimpleDateFormat;

import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean(name="holaMundoMBean")
@SessionScoped
public class HolaMundoMBean implements Serializable {
    private static final long serialVersionUID = -239729940657225276L;
    
    private String name;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getCurrentTime() {
         
        return new SimpleDateFormat("dd/MM/yyyy HH:mm:ss").format(new java.util.Date().getTime()); // Older version, SimpleDateFormat is not thread safe
    }
    
    
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("HolaMundoMBean [name=");
        builder.append(name);
        builder.append("]");
        return builder.toString();
    }    
    
}

JSF 2.x Páginas dinámicas

El formato recomendado al desarrollar páginas dinámicas en JSF es el formato XHTML (Extensible HTML) .

Para el ejemplo de hola mundo voy a realizar dos páginas dinámicas; holamundo.xhtml y holamundobienvenida.xhtml. Desde la página inicial se viaja a la de bienvenida llevándose la información del nombre para que la bienvenida sea personalizada.

Fichero: holamundo.xhtml

<?xml version="1.0" encoding="ISO-8859-1" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html">
<h:head>
    <title>Aplicación JSF 2.x</title>
</h:head>
<h:body bgcolor="white">
    <h2>Ejemplo. Hola Mundo en JSF 2.x</h2>
    <h:form>
        <h3>Fecha y hora: <h:outputLabel> #{holaMundoMBean.currentTime}</h:outputLabel></h3>
        <h3>A continuacion escribe tu nombre en el siguiente campo:</h3>
        <h:inputText value="#{holaMundoMBean.name}"/>
        <h:commandButton value="Pulsa" action="holamundobienvenida"/>
    </h:form>
</h:body>
</html>

Fichero: holamundobienvenida.xhtml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
 
    <h:head>
        <title>Aplicación JSF 2.x</title>
    </h:head>
    <h:body bgcolor="cyan">
        <h2>Ejemplo. Hola Mundo en JSF 2.x</h2>
        <h3>Hola #{holaMundoMBean.name}, bienvenido al ejemplo Hola Mundo con JSF 2.x.</h3>
        <h3>Fecha y hora: #{holaMundoMBean.currentTime} </h3>
        <h3>Hasta Luego.</h3>
        <h:form>
            <h:commandButton value="Volver" action="holamundo"/>
        </h:form>
    </h:body>
</html>

Hay que fijarse en las siguientes peculiaridades:

  • En JSF 1.x es obligatorio declarar una «navigation rule» en el fichero faces-config.xml para indicarle a la aplicación a qué página hay que ir cuando se pulsa un botón. Sin embargo, en JSF 2.x para facilitar la navegación se puede poner el nombre de la página directamente en el atributo action del botón. En aplicaciones pequeñas esta ventaja es un buen recurso. En aplicaciones grandes es recomendable seguir utilizando el fichero faces-config.xml para definir las reglas de navegación.
  • El campo #{…} indica una expresión dinámica de JSF que se interpreta en tiempo de ejecución. En la página holamundo.xhtml la expresión  #{holaMundoMBean.name} en el componente inputText indica que cuando se hace submit del formulario, JSF encuentra el Managed Bean holaMundoMBean, y en la propiedad name recoge el valor del componente mediante el método set correspondiente. Cuando en la página holamundobienvenida.xhtml se ejecute la expresión #{holaMundoMBean.name}, esta accede a la propiedad correspondiente del Managed Bean y recupera el valor incluido en la anterior página.

Resultado final.

A continuación se pueden ver los pantallazos con la aplicación funcionando.

En la primera página se teclea el nombre y se pulsa el botón de submit del formulario.

2016-08-29_14-45-14

Una vez tecleado y cuando se pulsa el botón se muestra la página de bienvenida.

2016-08-29_14-46-20

Si se pincha el botón Volver se regresa a la página holamundo.xhtml.

Links

Java Server Faces en la Wikipedia

Java EE Historial de versiones

Extensible HTML

Minientrada

Tomcat 8.5 en Eclipse Neon 4.6.0 (Build id: 20160613-1800)

No existe Runtime Enviroment para Tomcat 8.5 en Eclipse Neon.

Me descargué el otro día la nueva versión de Eclipse para comenzar mis nuevos proyectos basados en JEE 6 con un entorno de desarrollo puesto al día. Adicionalmente descargué también la última versión de Apache Tomcat 8.5 para probar los trabajos.

Mi sopresa surgió cuando al enlazar la versión de Eclipse con la Server Runtime de Apache Tomcat 8.5, encuentro que puedo seleccionar la versión 8.0 y la 9.0, pero no la susodicha.

2016-08-25_10-57-12

Al seleccionar cualquiera de las otras dos 8.0 o 9.0 obtengo el error «The Apache Tomcat installation at this directory is version 8.5.4. A Tomcat 8.0 (9.0 si selecciono la 9) installation is expected«.

Buscando en la base de datos Bugzilla de Eclipse encuentro el bug 494936. En el se comenta  de forma resumida que Eclipse Neon todavía no está preparado para Apache Tomcat 8.5 y que está previsto para futuras versiones.

Una solución temporal.

Para salir del paso, en el propio caso abierto de bugzilla, Levan Kekelidze proporciona una nueva versión del plugin de tomcat que permite añadir Tomcat 8.5 como si fuera la versión 9.0 del server runtime.

La versión de este plugin está publicado por Levan como attachment y se puede descargar directamente desde este enlace.

Se reemplaza el fichero org.eclipse.jst.server.tomcat.core_1.1.800.v201602282129.jar que se encuentra en <ECLIPSE_NEON_INSTALLATION_DIR>\plugins por el fichero descargado en el enlace previo.

Et voila!!! Ya podemos agregar una instancia de Apache Tomcat 8.5 como si fuera de tipo Apache Tomcat 9.0.

2016-08-25_11-13-11

Mientras sale el parche que permita agregar de manera nativa la Server Runtime Enviroment para Apache Tomcat 8.5 se puede continuar trabajando de manera temporal con esta solución. Con esta «solución», todo funciona correctamente, se puede arrancar, parar, depurar y desplegar aplicaciones desde Eclipse al servidor.

Espero que esta entrada os sea de utilidad.

 

Extraer un XSD a partir de un XML existente

A veces me he encontrado en la tesitura de tener un XML maravilloso con toda la información que necesito y querer explotar su información mediante Java.

Si utilizamos JAXB, el estándar de Java para el tratamiento XML-JAVA, veremos que el compilador xjc, incluido en la JDK (a partir de la versión 1.6.0_3), necesita un XSD para generar los objetos java correspondientes.

Por tanto, el primer paso, y el motivo de esta entrada, es tratar las diferentes maneras que hay de convertir el XML en un XSD.

Voy a utilizar para la generación del XSD un XML de ejemplo sencillo, pero suficiente, para ver las diferencias entre las herramientas presentadas.

<?xml version="1.0" encoding="UTF-8"?>
<servers>
	<server id="svnserver.com">
		<ip>192.168.1.1</ip>
		<osuser>souser</osuser>
		<ospass>sopass</ospass>
		<repositories>
			<repository name="a53">
				<url>http://svnserver.com/svn/a53</url>
				<svnuser>user</svnuser>
				<svnpass>pass</svnpass>
			</repository>
			<repository name="i07">
				<url>http://svnserver.com/svn/i07</url>
				<svnuser>user</svnuser>
				<svnpass>pass</svnpass>
			</repository>
		</repositories>
	</server>
	<server id="svnserver2.com">
		<ip>192.168.1.2</ip>
		<osuser>souser</osuser>
		<ospass>sopass</ospass>
		<repositories>
			<repository name="a53_backup">
				<url>http://svnserver.com/svn/a53_backup</url>
				<svnuser>user</svnuser>
				<svnpass>pass</svnpass>
			</repository>
		</repositories>
	</server>
</servers>

Trang.jar (http://www.thaiopensource.com/relaxng/trang.html)

Este programa no se actualiza desde 2008, pero tampoco es preocupante porque tampoco ha cambiado la especificación W3C de los XML Schemas. Se puede descargar directamente desde su página web y es muy sencillo de utilizar.

usage: java -jar trang.jar 
		[-I rng|rnc|dtd|xml] 
		[-O rng|rnc|dtd|xsd] 
		[-i input-param] 
		[-o output-param] 
		inputFileOrUri ... outputFile

Partiendo del XML de ejemplo propuesto, la respuesta de trang ha sido la siguiente:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
  <xs:element name="servers">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" ref="server"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="server">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="ip"/>
        <xs:element ref="osuser"/>
        <xs:element ref="ospass"/>
        <xs:element ref="repositories"/>
      </xs:sequence>
      <xs:attribute name="id" use="required" type="xs:NCName"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="ip" type="xs:NMTOKEN"/>
  <xs:element name="osuser" type="xs:NCName"/>
  <xs:element name="ospass" type="xs:NCName"/>
  <xs:element name="repositories">
    <xs:complexType>
      <xs:sequence>
        <xs:element maxOccurs="unbounded" ref="repository"/>
      </xs:sequence>
    </xs:complexType>
  </xs:element>
  <xs:element name="repository">
    <xs:complexType>
      <xs:sequence>
        <xs:element ref="url"/>
        <xs:element ref="svnuser"/>
        <xs:element ref="svnpass"/>
      </xs:sequence>
      <xs:attribute name="name" use="required" type="xs:NCName"/>
    </xs:complexType>
  </xs:element>
  <xs:element name="url" type="xs:anyURI"/>
  <xs:element name="svnuser" type="xs:NCName"/>
  <xs:element name="svnpass" type="xs:NCName"/>
</xs:schema>

El XSD generado es un XSD básico, sencillo y útil. Lo único que no termina de convencerme es la tipificación xs:NCName (Non Colonized Name) que le ha dado a las cadenas de texto. No es una tipificación incorrecta, pero limita el uso de qualified names (nombres con namespace) en el contenido del texto.

inst2xsd (http://xmlbeans.apache.org/index.html)

La herramienta inst2xsd viene incluida en el software XMLBEANS de Apache Foundation que se puede descargar directamente desde su página web. Esta herramienta es más completa que Trang, aunque sin ser difícil también es más compleja de utilizar.

Usage: inst2xsd [opts] [instance.xml]*
Options include:
    -design [rd|ss|vb] - XMLSchema design type
             rd  - Russian Doll Design - local elements and local types
             ss  - Salami Slice Design - global elements and local types
             vb  - Venetian Blind Design (default) - local elements and global complex types
    -simple-content-types [smart|string] - Simple content types detection (leaftext). Smart is the default
    -enumerations [never|NUMBER] - Use enumerations. Default value is 10.
    -outDir [dir] - Directory for output files. Default is '.'
    -outPrefix [file_name_prefix] - Prefix for output file names. Default is 'schema'
    -validate - Validates input instances agaist generated schemas.
    -verbose - print more informational messages
    -license - print license information
    -help - help imformation

Para generar el siguiente XML he utilizado la opción -enumerations never para evitar que los valores que se incluyen en el ejemplo se convierten en enumeraciones en el XSD final.

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="servers" type="serversType"/>
  <xs:complexType name="repositoryType">
    <xs:sequence>
      <xs:element type="xs:anyURI" name="url"/>
      <xs:element type="xs:string" name="svnuser"/>
      <xs:element type="xs:string" name="svnpass"/>
    </xs:sequence>
    <xs:attribute type="xs:string" name="name" use="optional"/>
  </xs:complexType>
  <xs:complexType name="repositoriesType">
    <xs:sequence>
      <xs:element type="repositoryType" name="repository" maxOccurs="unbounded" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="serverType">
    <xs:sequence>
      <xs:element type="xs:string" name="ip"/>
      <xs:element type="xs:string" name="osuser"/>
      <xs:element type="xs:string" name="ospass"/>
      <xs:element type="repositoriesType" name="repositories"/>
    </xs:sequence>
    <xs:attribute type="xs:string" name="id" use="optional"/>
  </xs:complexType>
  <xs:complexType name="serversType">
    <xs:sequence>
      <xs:element type="serverType" name="server" maxOccurs="unbounded" minOccurs="0"/>
    </xs:sequence>
  </xs:complexType>
</xs:schema>

El XSD generado con esta herramienta es aún más simple que el generado con Trang. Los objetos que contienen texto los ha tipificado como xs:string que puede contener cualquier tipo de texto.

Conclusión

Existen más herramientas en el mercado para obtener ficheros XML Schema a partir de ficheros XML. Algunas son de pago, otras son más complejas, otras tienen interfaz gráfico, pero las dos herramientas descritas en esta entrada son suficientes para obtener XSD de calidad de forma fácil y gratuita.

Si bien me ha gustado más la generación de la herramienta inst2xsd por la generación menos restrictiva de las cadenas de texto, ambas herramientas han generado XSD aptos para utilizar con JAXB y poder generar las clases Java necesarias para tratar la información del XML.

Links

Xml Schema W3C
NCName

Buenas prácticas JAXWS. Utilizando RequestWrapper.

Desde la aparición de la arquitectura SOA (Service-Oriented Architecture) y la posterior llegada de SCA (Service Component Architecture), la utilización de servicios web se ha multiplicado en los desarrollos software de las empresas.

En el mundillo java surgieron diferentes tecnologías para facilitar la creación de los servicios web (Axis1, Axis2, CXF, etcétera), pero a raíz de la llegada de la implementación de la especificación JSR 224 – JAXWS a partir de la JDK 1.5, es cuando la creación de servicios web se ha hecho más inter-operable y gracias a la utilización de anotaciones se ha facilitado enormemente.

En esta entrada me centro en la anotación JAXWS @javax.xml.ws.RequestWrapper, como usarla y la utilidad que tiene.

Un caso de uso.

Cuando se realiza un servicio web con JAX-WS se crea una clase java con anotaciones JSR 181 (@WebService, @WebMethod, etcétera) que describen el contrato WSDL que luego utilizan los clientes para invocar al servicio.

Para explicar esta entrada, voy a utilizar el siguiente ejemplo de servicio web básico, compuesto por un método que recibe una serie de parámetros.

package com.egv.webservices.jaxws;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService ( name="WebServiceEjemplo", 
              targetNamespace="http://egv.com/webservices/jaxws")
@SOAPBinding(  style=SOAPBinding.Style.DOCUMENT, 
               use=SOAPBinding.Use.LITERAL, 
               parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
public class WebServiceEjemplo {
	
	@WebMethod(operationName="metodoEjemplo")
	public void metodoEjemplo(  @WebParam(name="codigo1")String codigo1,
                                @WebParam(name="descripcion1")String descripcion1,
                                @WebParam(name="codigo2")Integer codigo2,
                                @WebParam(name="descripcion2")String descripcion2) {
		System.out.println("codigo1=" + codigo1);
		System.out.println("descripcion1=" + codigo1);
		System.out.println("codigo2=" + codigo2);
		System.out.println("descripcion2=" + descripcion2);
	}

}

Esta clase java genera un método metodoEjemplo en el servicio web con las siguiente Request de tipo SOAP:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://egv.com/webservices/jaxws">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:metodoEjemplo>
         <!--Optional:-->
         <codigo1>?</codigo1>
         <!--Optional:-->
         <descripcion1>?</descripcion1>
         <!--Optional:-->
         <codigo2>?</codigo2>
         <!--Optional:-->
         <descripcion2>?</descripcion2>
      </jax:metodoEjemplo>
   </soapenv:Body>
</soapenv:Envelope>

Hasta aquí el caso más básico de creación de un servicio web. Pero, qué ocurre si queremos complicarlo haciendo que los parámetros codigo1 y codigo2 se requieran de forma obligatoria.

Para conseguir esta obligatoriedad podemos optar por dos tipos de soluciones: una aproximación a posteriori o una aproximación a priori.

La aproximación a posteriori

La solución a posteriori se introduce una vez que el servicio web se ha invocado y se consigue añadiendo en el código una comprobación de parámetros mediante condicionales. Por ejemplo:

if (codigo1 == null || codigo2 == null) {
throw new ParametrosObligatoriosException();
}

Esta comprobación es funcionalmente correcta pero no es óptima. El motivo es que para realizar la comprobación de parámetros se hace una llamada al servicio web, la ejecución del método y el envío de la respuesta.

La aproximación a priori

La solución a priori se basa en el uso conjunto de la anotación javax.xml.ws.RequestWrapper de JAXWS combinado con el uso de las anotaciones JSR 222 – JAXB. Esto puede sonar complejo, e inicialmente nos va a obligar a codificar más líneas de código, sin embargo, la flexibilidad que proporciona a la hora de jugar con los parámetros, tipo, obligatoriedad, etcétera, es mucho más óptima.

El primer paso para llevar a cabo esta solución es hacer un wrapper para envolver la request del método. Este wrapper debe contener los campos declarados como parámetros del método con la anotación WebParam. Cada campo deber ir anotado con anotaciones JAXB que indican las características del campo concreto.

package com.egv.webservices.jaxws;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlType;

@XmlType(propOrder = { "codigo1", "descripcion1", "codigo2", "descripcion2" })
@XmlAccessorType(XmlAccessType.FIELD)
public class WebServiceEjemploWrapperRequest {
	
	@XmlElement(name="codigo1", required=true)
	public String codigo1;

	@XmlElement(name="descripcion1")
	public String descripcion1;

	@XmlElement(name="codigo2", required=true)
	public Integer codigo2;

	@XmlElement(name="descripcion2")
	public String descripcion2;

	[…] //Getter y Setter para cada variable.

La anotación @XmlElement indica el nombre de la variable y en las variables codigo1 y codigo2 además incluye el indicador de obligatoriedad required=true.

El segundo paso es anotar el método del servicio web en su clase con la anotación RequestWrapper

	@WebMethod(operationName="metodoEjemplo")
	@RequestWrapper(localName = "metodoEjemploRequestWrapper", 
    className = "com.egv.webservices.jaxws.WebServiceEjemploWrapperRequest")
	public void metodoEjemplo(  @WebParam(name="codigo1")String codigo1,
                                @WebParam(name="descripcion1")String descripcion1,
                                @WebParam(name="codigo2")Integer codigo2,
                                @WebParam(name="descripcion2")String descripcion2) {

El resultado final podemos observarlo en la request SOAP generada:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://egv.com/webservices/jaxws">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:metodoEjemploRequestWrapper>
         <codigo1>?</codigo1>
         <!--Optional:-->
         <descripcion1>?</descripcion1>
         <codigo2>?</codigo2>
         <!--Optional:-->
         <descripcion2>?</descripcion2>
      </jax:metodoEjemploRequestWrapper>
   </soapenv:Body>
</soapenv:Envelope>

Los campos codigo1 y codigo2 han dejado de ser opcionales y ahora han pasado a ser obligatorios. Sin embargo, si ejecutamos desde un cliente y dejamos sin informar alguno de los campos obligatorios, el servicio web se ejecuta y da respuesta.

El método se ejecuta pese a no rellenar los campos obligatorios

La explicación a este supuesto mal funcionamiento es que la validación del esquema del servicio web consume bastantes recursos afectando al rendimiento y por tanto no se activa por defecto. Si se quiere activar la validación del esquema bastará con añadir la anotación com.sun.xml.ws.developer.SchemaValidation a nivel de servicio web.

@WebService (name="WebServiceEjemplo", targetNamespace="http://egv.com/webservices/jaxws")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT, 
			use=SOAPBinding.Use.LITERAL, 
			parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
@SchemaValidation
public class WebServiceEjemplo {
[…]

Una vez añadida la anotación, al invocar el servicio web sin rellenar el campo codigo1, la respuesta es:

   <S:Body>
      <S:Fault xmlns:ns4="http://www.w3.org/2003/05/soap-envelope">
         <faultcode>S:Server</faultcode>
         <faultstring>com.sun.istack.XMLStreamException2: org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'descripcion1'. One of '{codigo1}' is expected.</faultstring>
         <detail>
            <ns2:exception class="javax.xml.ws.WebServiceException" note="To disable this feature, set com.sun.xml.ws.fault.SOAPFaultBuilder.disableCaptureStackTrace system property to false" xmlns:ns2="http://jax-ws.dev.java.net/">
               <message>com.sun.istack.XMLStreamException2: org.xml.sax.SAXParseException: cvc-complex-type.2.4.a: Invalid content was found starting with element 'descripcion1'. One of '{codigo1}' is expected.</message>
               <ns2:stackTrace>
                  <ns2:frame class="com.sun.xml.ws.util.pipe.AbstractSchemaValidationTube" file="AbstractSchemaValidationTube.java" line="242" method="doProcess"/>
[…]

La validación del esquema hace también comprobaciones de tipos de datos, de forma que si en el campo Integer introducimos ‘abc’ se lanzará una excepción.

<message>com.sun.istack.XMLStreamException2: org.xml.sax.SAXParseException: cvc-datatype-valid.1.2.1: 'abc' is not a valid value for 'integer'.</message>

Ideas a evitar

Se podría idear una solución intermedia sin utilizar la anotación RequestWrapper. Esta solución intermedia pasaría por incluir los campos de la request en un objeto wrapper, como el objeto WebServiceEjemploWrapperRequest del ejemplo, y hacer que este objeto sea el único parámetro del método.

	@WebMethod(operationName="metodoMalEjemplo")
	public void metodoMalEjemplo(	@WebParam(name="objetoComplejo")WebServiceEjemploWrapperRequest objetoComplejo) {
		System.out.println("codigo1=" + objetoComplejo.getCodigo1());
		System.out.println("descripcion1=" + objetoComplejo.getDescripcion1());
		System.out.println("codigo2=" + objetoComplejo.getCodigo2());
		System.out.println("descripcion2=" + objetoComplejo.getDescripcion2());
	}

Esta programación dará como resultado la siguiente petición SOAP:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:jax="http://egv.com/webservices/jaxws">
   <soapenv:Header/>
   <soapenv:Body>
      <jax:metodoMalEjemplo>
         <!--Optional:-->
         <objetoComplejo>
            <codigo1>?</codigo1>
            <!--Optional:-->
            <descripcion1>?</descripcion1>
            <codigo2>?</codigo2>
            <!--Optional:-->
            <descripcion2>?</descripcion2>
         </objetoComplejo>
      </jax:metodoMalEjemplo>
   </soapenv:Body>
</soapenv:Envelope>	

El funcionamiento de los clientes que llaman al método con la request configurada de esta manera es similar a la configuración con RequestWrapper. Sin embargo, añadir el encapsulado adicional <objetoComplejo> no es una buena idea ya que cuanto más simple permanezca el modelo más compatible será éste.

Conclusión

La utilización de la anotación RequestWrapper permite utilizar JAXB para configurar las características de cada campo que participa en la request de un método de un servicio web.

Esta anotación permite generar una request lo más simple posible lo que, entre otras cosas, hará que los métodos sean completamente compatibles con sistemas de cacheo que normalmente no funcionan bien con objetos complejos.

Links

Publicar servicios web JAX-WS 2.1 en Tomcat 6

La llegada de la crisis se está dejando ver en todos los ámbitos y la informática no es una excepción. Hasta hace no mucho las empresas no daban excesiva importancia al coste de sus servidores y se pagaban las licencias de software religiosamente. Pero, la crisis ha dado un vuelco a esta situación. El gasto se controla más y allí donde antes se utilizaba un servidor de pago para dar servicio, ahora se busca sustituirlo por versiones de código abierto y gratuitas.

Concretamente, en el cliente para el que trabajo actualmente se ha decido empezar a utilizar Apache Tomcat para aquellas aplicaciones que no necesitan de la pila Enterprise de Java.

Esta entrada recoge cómo publicar servicios web JAX-WS en un Apache Tomcat 6.x.

Requisitos

Antes de comenzar con el código puro y duro voy a hacer un pequeño repaso del entorno necesario para seguir la solución propuesta en la entrada.

La máquina virtual Java es la Jrockit-jdk1.6.0_45-R28.2.7-4.1.0. Utilizo esta versión porque en los entornos productivos es esta máquina virtual la que se instala. El ejemplo puede funcionar igualmente con la HotSpot 1.6. La versión en concreto que estoy utilizando contiene la versión JAX-WS RI 2.1.6. Si quieres averiguar que versión de JAX-WS incorpora tu máquina virtual basta con ejecutar el siguiente comando:

${java-home}/bin/wsimport -version

El servidor Tomcat elegido para las pruebas es la versión 6.0.37. No utilizo la versión 7 de Tomcat porque los entornos productivos están configurados con la versión Tomcat 6. Esta versión admite las especificaciones Servlet/2.5, JSP/2.1 y requiere la versión 1.5 o versiones superiores de la máquina virtual Java.

Instalación básica de Tomcat.

El primer paso es realizar una instalación básica de Tomcat.

El servidor se puede descargar de la página oficial de Apache Tomcat. La instalación básica es muy sencilla, basta con descomprimir el fichero descargado en un directorio que a partir de ahora voy a llamar ${TOMCAT_HOME}.

Antes de arrancar el servidor hay que configurar el usuario administrador para poder acceder a la aplicación de despliegue de Tomcat. Los usuarios de Tomcat se configuran en el fichero ${TOMCAT_HOME}/conf/tomcat-users.xml. En este fichero se añaden las siguientes líneas:

<role rolename="manager"/>
<role rolename="admin"/>
<user username="admin" password="admin" roles="admin,manager"/>

No tendría que decirlo, pero por si acaso, ni que decir tiene que el nombre del usuario administrador así como su password no tiene porque coincidir con el ejemplo, queda a elección de la persona que instala el servidor crear los roles y usuarios que más le convengan.

Para el ejemplo básico creo el rol manager, el rol admin y el usuario admin que estará compuesto por estos dos roles.

Una vez modificado este fichero se puede poner en marcha el servidor. Para arrancar y parar, el propio servidor proporciona dos scripts que facilitan enormemente la tarea. El script de arranque es ${TOMCAT_HOME}/bin/startup.bat (en entornos Windows) o ${TOMCAT_HOME}/bin/startup.sh (en entornos unix). Para parar el servidor el script es ${TOMCAT_HOME}/bin/shutdown.bat (Windows) y ${TOMCAT_HOME}/bin/shutdown.sh (Unix).

Creación de la aplicación de ejemplo con servicios web JAXWS.

La aplicación de ejemplo se crea utilizando Maven 3.x para facilitar el manejo del ciclo de vida en general y de las dependencias en particular.

Desde Eclipse se crea un nuevo Maven Project a partir del arquetipo org.apache.maven.archetypes – maven-archetype-webapp en su versión RELEASE. Esto proporciona la base de una aplicación web dinámica. La estructura se parece a lo que se ve en la siguiente captura:

Arquitectura creada por Maven para una aplicación web.

Para facilitar el desarrollo, publicación y pruebas de la aplicación se incluye en el fichero pom.xml que ha generado el arquetipo la configuración de dos plugins; maven-war-plugin y maven-clean-plugin.

El plugin maven-war-plugin configura el empaquetado WAR de la aplicación con ciertas características:

  • Se va a publicar la aplicación directamente en el directorio de despliegues por defecto de Tomcat. Esto se hace añadiendo el tag <webappDirectory> en las configuración del plugin y permite hacer pruebas inmediatas de la aplicación sin necesidad de estar publicándola constantemente desde el Deployer.
  • Se va a dejar una versión empaquetada en un fichero WAR en el directorio dist de la aplicación. Esto se consigue con el tag <outputDirectory>. La idea es poder tener un WAR independiente para poder enviar la aplicación a otros entornos de forma fácil.
<build>
    […]
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-war-plugin</artifactId>
        <version>2.3</version>
        <configuration>
          <webappDirectory>E:\srv\dominio\aplic\dominio_tomcat\apache-tomcat-6.0.37\webapps\ExampleWS</webappDirectory>
          <outputDirectory>./dist</outputDirectory>
          <warName>ExampleWS-${version}</warName>
        </configuration>
      </plugin>
    </plugins>
	 […]
</build>

El plugin maven-clean-plugin limpia los compilados de la aplicación. Como se ha añadido la generación del fichero war en un directorio específico, se configura este plugin para que lo elimine cuando se ejecute un mvn clean.

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-clean-plugin</artifactId>
    <version>2.5</version>
    <configuration>
      <filesets>
         <fileset>
          <directory>./dist</directory>
          <followSymlinks>false</followSymlinks>
          <useDefaultExcludes>true</useDefaultExcludes>
          <includes>
            <include>*.war</include>
          </includes>
        </fileset>
      </filesets>
    </configuration>
  </plugin>

Adicionalmente, se añaden las dependencias necesarias para que la aplicación entienda y publique los servicios web JAXWS:

  • jaxws-rt v2.1.6
  <dependencies>
  […]
    <dependency>
      <groupId>>com.sun.xml.ws</groupId>
      <artifactId>jaxws-rt</artifactId>
      <version>2.1.7</version>
    </dependency>
  […]
  </dependencies>

Creación el servicio web de ejemplo.

Desde que la JDK 1.5 incorporó las anotaciones y JAX-WS hizo uso de ellas, crear un servicio web de ejemplo se ha convertido en una tarea sencilla.

package com.egv.webservices.jaxws;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService (name="WebServiceUno", targetNamespace="http://egv.com/webservices/jaxws")
@SOAPBinding(style=SOAPBinding.Style.DOCUMENT, 
			use=SOAPBinding.Use.LITERAL, 
			parameterStyle=SOAPBinding.ParameterStyle.WRAPPED)
public class WebServiceUno {
	
	@WebMethod(operationName="HolaMundo")
	public void HolaMundo(@WebParam(name="nombre")String nombre) {
		System.out.println("Hola mundo! Te saluda " + nombre);
	}

}  

Este servicio web se llama WebServiceUno y tiene un único método que se llama HolaMundo que recibe un nombre como parámetro y que una vez ejecutado escribe en consola Hola mundo! te saluda ${nombre}. Un servicio web muy simple pero perfecto para probar la publicación de WS JAXWS en Tomcat 6.

Configuración de la aplicación para publicar el servicio web en Tomcat.

Para publicar los servicios web en tomcat es necesario que la aplicación publique un servlet específico de JAXWS en el arranque. Este servlet se encuentra en el paquete com.sun.xml.ws.transport.http.servlet.WSServlet y viaja con la implementación de JAXWS que se ha descargado con la dependencia oportuna. En el fichero web.xml de la aplicación se incluye el siguiente código:

<web-app>
  	[…]
    <servlet>
        <servlet-name>WebServiceUno</servlet-name>
        <servlet-class>
        	com.sun.xml.ws.transport.http.servlet.WSServlet
        </servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>WebServiceUno</servlet-name>
        <url-pattern>/wsUno</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>120</session-timeout>
    </session-config>
	[…]  
</web-app>

El servlet WSServlet ejecuta los servicios web que se incluyan en la aplicación. Pero, para que Tomcat sepa que servicios web tiene que publicar hay que incluir el fichero sun-jaxws.xml. Este fichero contiene la configuración de los endpoints de los servicios web.

<?xml version="1.0" encoding="UTF-8"?>
<endpoints
  xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime"
  version="2.0">
  <endpoint
      name="WebServiceUno"
      implementation="com.egv.webservices.jaxws.WebServiceUno"
      url-pattern="/wsUno"/>
</endpoints>

Tomcat entiende este fichero gracias al listener com.sun.xml.ws.transport.http.servlet.WSServletContextListener específico de JAXWS. Este listener es el encargado de parsear el fichero y crear un HttpAdapter por cada endpoint definido. El listener hay que incluirlo en la aplicación añadiendo las siguientes líneas en el fichero web.xml:

<web-app>
[…]
<listener>
        <listener-class>
                com.sun.xml.ws.transport.http.servlet.WSServletContextListener
        </listener-class>
    </listener> 
[…]
</web-app>

El war generado presenta la siguiente estructura:

Estructura del fichero War.

Desplegar y probar.

Una vez que todo está creado, se ejecuta mvm install.

Si la compilación es correcta, ésta ejecución debería dejar en el directorio de despliegue de tomcat la aplicación correctamente publicada. En el deployer de tomcat se verá la aplicación correctamente publicada.

Aplicación correctamente desplegada.

Accediendo a la URI del servicio web se ve el servicio web publicado.

Web Service correctamente desplegado.

Probando la aplicación a través de SOAPUi se comprueba el funcionamiento correcto.

Ejecución desde SOAPUi

En la consola del servidor encontramos la traza escrita.

Consola de salida del servidor.

Conclusión

Como se ha visto a lo largo de la entrada, no es especialmente difícil publicar servicios web JAXWS en Apache Tomcat. La ventaja es que Tomcat cuenta con una licencia de código abierto ahorrando a las empresas bastantes euros en costosas licencias de servidores de aplicación.

Por tanto, si la aplicación que se desea desarrollar no tiene exigencias Enterprise y va a tener una carga normal de trabajo, ésta configuración se puede utilizar sin problemas.

CustomBinding en ServiceEndpoint con el contrato ‘IServicioWeb’ carece de TransportBindingElement

El otro día generando un cliente .NET para un servicio web JAXWS basado en SOAP 1.2 me topé con el siguiente problema:

System.InvalidOperationException: 
CustomBinding en ServiceEndpoint con el contrato 'IServicioWeb' carece de TransportBindingElement. 
Todos los enlaces deben tener al menos un elemento de enlace que se derive de TransportBindingElement.

O lo que es lo mismo pero en la lengua de Shakespeare:

System.InvalidOperationException: 
The CustomBinding on the ServiceEndpoint with contract 'Contract Name' lacks a TransportBindingElement. 
Every binding must have at least one binding element that derives from TransportBindingElement.

El servicio web se había generado desde Visual Studio utilizando la herramienta Add Service Reference, que por debajo hace uso de SvcUtil.exe, siguiendo los pasos que en su momento dejé recogidos en la entrada «Generando clientes de servicios web JAX-WS desde .NET».

Leyendo detenidamente el mensaje de error, se llega a la conclusión de que la sección customBinding no tiene referencia a ningún TransportBindingElement.

TransportBindingElement es una clase abstracta que representa un tipo de binding (enlace) de transporte. Los tipos de binding de transporte más comunes son:

  • Peer to peer – PeerTransportBindingElement
  • Http – HttpTransportBindingElement
  • Https – HttpsTransportBindingElement
  • TCP – TcpTransportBindingElement
  • Named Pipe – NamedPipeTransportBindingElement
  • MS MQ – MsmqTransportBindingElement
  • MS MQ Integration – MsmqIntegrationBindingElement
  • Connection Oriented – ConnectionOrientedTransportBindingElement

En este caso, el acceso al servicio web se realiza utilizando protocolo HTTP, por lo que hay que añadir una referencia a HttpTransportBindingElement en la configuración del cliente.

La manera simple de incluir esta información es en el fichero app.config de la aplicación.

Tras la generación con la herramienta SvcUtil.exe de Visual Studio el app.config tiene el siguiente aspecto:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <customBinding>
                <binding name="ServicioWebPortBinding">
                    <textMessageEncoding messageVersion="Soap12" />
                </binding>
            </customBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost:8080/ServicioWebWSImpl/ServicioWeb"
                binding="customBinding" bindingConfiguration="ServicioWebPortBinding"
                contract="ServicioWebSR.ServicioWeb" name="ServicioWebPort" />
        </client>
    </system.serviceModel>
</configuration>

La solución al problema es añadir la configuración <httpTransport> en la definición del <CustomBinding> del fichero app.config.

La sintaxis de esta configuración es:

<httpTransport
    allowCookies=Boolean"
    authenticationScheme="Digest/Negotiate/Ntlm/Basic/Anonymous"
    bypassProxyOnLocal=Boolean"
    hostnameComparisonMode="StrongWildcard/Exact/WeakWildcard"
    keepAliveEnabled="Boolean"
    maxBufferSize="Integer"
    proxyAddress="Uri"
    proxyAuthenticationScheme="None/Digest/Negotiate/Ntlm/Basic/Anonymous"
IntegratedWindowsAuthentication: Specifies Windows authentication"
    realm="String"
    transferMode="Buffered/Streamed/StreamedRequest/StreamedResponse"
        unsafeConnectionNtlmAuthentication="Boolean"
        useDefaultWebProxy="Boolean" />  
  • allowCookies – Un valor booleano que especifica si el cliente acepta las cookies y las propaga en solicitudes futuras. El valor predeterminado es false.
  • authenticationScheme – Especifica el protocolo utilizado para autenticar solicitudes de cliente que son procesadas por un agente de escucha HTTP. El valor predeterminado es anónimo. Protocolos de autenticación válidos pueden ser:
    • Digest: especifica la autenticación implícita.
    • Negotiate: negocia con el cliente para determinar el esquema de autenticación. Si cliente y el servidor son compatibles con Kerberos, se utiliza; de lo contrario, se utiliza NTLM
    • Ntlm: especifica la autenticación NTLM.
    • Basic: especifica la autenticación básica.
    • Anonymous: especifica la autenticación anónima.
  • bypassProxyOnLocal – Un valor booleano que indica si se omitirá el servidor proxy para las direcciones locales. El valor predeterminado es false.
  • hostnameComparisonMode – Especifica el modo de comparación de HostName HTTP usado para analizar los URI. El valor predeterminado es StrongWildcard. Para analizar las URIs se puede hacer de tres maneras:
    • StrongWildcard: («+») coincide con todos los posibles nombres del host en el contexto del esquema especificado, puerto y URI relativo.
    • Exact: ningún carácter comodín
    • WeakWildcard: («*») coincide con todo posible nombre del host en el contexto del esquema especificado, puerto y URI relativo con los que no se han coincidido explícitamente o a través del mecanismo del carácter comodín fuerte.
  • keepAliveEnabled – Un valor booleano que especifica si se debe establecer una conexión continua con el recurso de Internet.
  • maxBufferSize – Un entero positivo que especifica el tamaño máximo del búfer. El valor predeterminado es 524288.
  • proxyAddress – Un URI que especifica la dirección del proxy HTTP. Si useSystemWebProxy es true, este valor debe ser null. El valor predeterminado es null.
  • proxyAuthenticationScheme – Especifica el protocolo utilizado para autenticar solicitudes de cliente que son procesadas por un proxy HTTP. La opción por defecto es Anonymous. Las diferentes posibilidades de autenticación son:
    • None: no se lleva a cabo ninguna autenticación.
    • Digest: especifica la autenticación implícita
    • Negotiate: negocia con el cliente para determinar el esquema de autenticación. Si cliente y el servidor son compatibles con Kerberos, se utiliza; de lo contrario, se utiliza NTLM
    • Ntlm: especifica la autenticación NTLM
    • Basic: especifica la autenticación básica
    • Anonymous: especifica la autenticación anónima
    • IntegratedWindowsAuthentication: especifica la autenticación de Windows
  • realm – Una cadena que especifica el dominio kerberos que se utilizará en el proxy/servidor. El valor predeterminado es una cadena vacía.
  • transferMode – Especifica si los mensajes se almacenan en búfer, se transmiten o si son una solicitud o una respuesta. El valor por defecto es Buffered. Las diferentes opciones de transferencia son:
    • Buffered: los mensajes de respuesta y solicitud están almacenados en búfer.
    • Streamed: se transmiten los mensajes de solicitud y respuesta
    • StreamedRequest: se transmite el mensaje de solicitud y el mensaje de respuesta está almacenado en búfer
    • StreamedResponse: se transmite el mensaje de respuesta y el mensaje de solicitud está almacenado en búfer
  • unsafeConnectionNtlmAuthentication – Un valor booleano que especifica si la conexión compartida no segura está habilitada en el servidor. El valor predeterminado es false. Si está habilitado, la autenticación NTLM se realiza una vez en cada conexión TCP.
  • useDefaultWebProxy – Un valor que especifica si se utiliza la configuración del proxy del equipo en lugar de la configuración específica del usuario. El valor predeterminado es true.

Conclusión

Todavía no tengo claro porque la generación automática del cliente no incluye la definición del tipo de transporte a utilizar en la comunicación. Lo que si tengo claro es que la solución es tan simple como añadir esta definición en el fichero de configuración de la aplicación.

        <bindings>
            <customBinding>
                <binding name="ServicioWebPortBinding">
                    <textMessageEncoding messageVersion="Soap12" />
			<httpTransport />
                </binding>
            </customBinding>
        </bindings>

Para este caso, y en vista de que la comunicación se hace por un canal HTTP sin proxy ni ninguna cosa rara, basta con añadir la etiqueta <httpTransport /> para solucionar el problema. Si existiera algún proxy entre el cliente y el servicio web, o algún sistema de autenticación, o se quisiera limitar las llamadas, se pueden ir añadiendo las posibilidades descritas a esta configuración.

Si la comunicación no fuera HTTP habría que generar otro tipo de binding de transporte, con sus propias configuraciones explicadas en la biblioteca MSDN de Microsoft.

Links

Usando yum detrás de un proxy

Uno de los grandes handicaps que más menudo sufro en el trabajo es estar detrás de un proxy. Siempre tengo que andar configurándolo por aquí y por allá, y por si fuera poco las configuraciones varían dependiendo de los programas que lo usan.

Una de estos programas «especiales» es la actualización del software mediante yum en los servidores con CentOS. Yum tiene como particularidad que no utiliza la configuración general del proxy que se realiza desde las utilidades gráficas de GNOME, y por tanto hay que configurarlo de manera personalizada.

Configurando la utilización de un proxy en yum a nivel de servidor.

Este método activa en yum la utilización del proxy de manera global para todos los usuarios del servidor. Pero, hay que tener en cuenta algunas cosillas.

Si el proxy tiene autenticación, este método requiere introducir el usuario y la password plana (sin cifrado) en un fichero de configuración que es accesible por cualquiera con un poco de conocimiento y algo de maña. Si la seguridad es importante, ya habéis deducido que este método puede no ser del todo recomendable.

También hay que tener en cuenta que cualquier usuario con permisos para ejecutar yum, hará actualizaciones usando la cuenta del proxy configurada.

Si las observaciones anteriores no disgustan, la activación del proxy para las operaciones con yum se hace en el fichero /etc/yum.conf añadiendo tres variables:

# /etc/yum.conf
# SERVER_PROXY CONFIGURATION
proxy=http://dns_servidor_proxy:puerto_servidor_proxy
proxy_username=usuario_proxy
proxy_password=password_proxy

Configurando la utilización de un proxy en yum para un usuario específico.

Si la anterior configuración no parecía adecuada porque la configuración del usuario y la password se realiza a nivel de servidor y es utilizada por cualquier usuario que ejecute yum, ésta configuración a nivel de usuario cuadrará mejor con las expectativas.

Con este método, cada usuario introducirá la configuración del proxy en el fichero ~/.bash_profile que está en su directorio home.

# ~/.bash_profile
# PERSONAL_PROXY_CONFIGURATION
http_proxy="http://usuario_proxy:password_proxy@dns_servidor_proxy:puerto_servidor_proxy"
export http_proxy

De esta manera, la ejecución de yum esta personalizada con la información de proxy que cada usuario ha introducido en su fichero personal.

Conclusión

Ambas configuraciones funcionan correctamente y permiten actualizar el servidor utilizando yum detrás de un proxy.

La elección de una u otra será una cuestión de qué configuración cuadra más con la política de seguridad que se quiere adoptar.

Y esto es todo, espero que sea útil.

Generando clientes de servicios web JAX-WS desde Java.

Hace poco publiqué una entrada donde comentaba la generación de clientes de servicios web JAX-WS desde .NET. Ha llegado el momento de hacer una entrada paralela para explicar el mismo procedimiento pero desde el punto de vista Java.

Existen varios métodos para generar clientes para un servicio web basado en tecnología JAX-WS 2.x, yo me voy a centrar en la herramienta wsimport que viene incluida en la implementación de referencia que Glassfish/Metro proporciona de la especificación.

La implementación de referencia.

La especificación JSR 224 – Java API for XML-Based Web Services establece las bases para trabajar con servicios web que utilizan XML para comunicarse. Como pasa con todas las especificaciones puede haber varias implementaciones, pero siempre hay una que se elige como la implementación de referencia (RI – Reference Implementation).

En el caso de ésta especificación la implementación de referencia la aporta Metro. La RI que yo voy a utilizar es la versión JAX-WS 2.1.9, por que se acerca mucho a la versión 2.1.5 que es la que incluye de saque el servidor de aplicaciones Weblogic 11g (parche 10.3.6), que es el que estoy utilizando para realizar estas pruebas.

En teoría, se podría utilizar cualquier versión de la implementación de referencia teniendo en cuenta que si difiere de la que incorpora weblogic habrá que incluir las librerías correctas en la aplicación y tal vez marcar la opción prefer-web-inf-classes en el descriptor weblogic.xml de nuestra aplicación.

La implementación de referencia se descarga de la página jax-ws.java.net (ver sección Links) y va empaquetada en un fichero JAR que si ejecutamos nos muestra un acuerdo de licencia y descomprime en la carpeta donde se ejecuta el contenido de la implementación.

La sintaxis de la herramienta wsimport

Los ejecutables de esta herramienta son insustancialmente diferentes en windows y en linux. En windows la herramienta está en \bin\wsimport.bat y en linux se puede encontrar en /bin/wsimport.sh.

La sintaxis en ambos sistemas es la misma:

wsimport [options] <wsdl>

<wsdl> indica una url que permita acceder al WSDL del servicio web para el que vamos a generar el cliente. Esta url puede ser tanto un recurso local, como un recurso obtenido mediante protocolo http.

[options] es el apartado donde se pueden incluir diferentes opciones que variarán el comportamiento de la herramienta. A continuación se presenta una lista completa de estas opciones:

  • -d <directory> : Indica el directorio de salida donde se dejan las clases compiladas. Si no se utiliza esta opción las clases compiladas se dejarán en el mismo directorio desde el que se llama a wsimport.
  • -b <path> : Añade ficheros XSD adicionales que se puedan necesitar en los binding jaxws/jaxb del servicio web.
  • -B <jaxbOption>
  • -catalog <file> : Especifica un fichero de catálogo que resuelve las referencias a entidades externas. Los formatos de catálogo soportados son: TR9401, XCatalog y OASIS XML Catalog.
  • -extension : Permite extensiones de terceros. Esta funcionalidad no está soportada por la especificación, por lo que el cliente generado puede no ser portable o permitir la interoperabilidad entre plataformas.
  • -help : Muestra una ayuda con el listado de las opciones.
  • -httpproxy:<host>:<port> : Si para acceder a la URL del WSDL que queremos generar hay que viajar a través de un proxy, con esta opción podremos indicar su configuración. Si no se rellena el puerto, por defecto será el 8080.
  • -keep : Si se incluye esta opción los fuentes que generan los compilados del cliente no se borran.
  • -p : Especifica el paquete java de las clases del cliente generado. Si se indica esta opción no se tendrán en cuenta; ni el nombre de paquete que puede incluirse en el wsdl, ni el nombre de paquete por defecto que se genera cuando no se indica esta opción.
  • -s <directory> : Especifica un directorio donde se guardan los ficheros de código fuente generados.
  • -verbose : Muestra los mensajes del compilador indicando las tareas que está realizando.
  • -version : Muestra un mensaje informativo con la versión de la implementación de referencia que se está utilizando.
  • -wsdllocation <location>
  • -target : Genera el código para la versión JAX-WS indicada. La versión 2.0 genera código compatible con la especificación JAX-WS 2.0.
  • -quiet: Elimina cualquier salida que se pueda generar. Útil para generaciones automatizadas de clientes.

Clases generadas

La herramienta wsimport genera las clases necesarias para poder invocar a las operaciones del servicio web de forma correcta. Las clases que se generan siguen siempre el siguiente criterio:

  • Clase PortType. Una clase que lleva el mismo nombre que el atributo name del elemento porttype del wsdl y contiene un método por cada operación definida con los elementos operation.
  • Clase Service. Una clase que lleva el mismo nombre que el atributo name del elemento service del wsdl. Esta clase accede al servicio web y permite instanciar la clase PortType.
  • Por cada operación definida en el portType
    • Tantas clases como sean necesarias para rellenar los Input
    • Tantas clases como sean necesarias para devolver el resultado de la operación
  • Clase ObjectFactory. Esta clase facilita la instanciación interna de las clases input y response.
  • Clase package-info. Anota el paquete java para que los objetos generados a partir del xsd del wsdl estén correctamente ubicados.

Una generación de ejemplo.

En la entrada Creando un servicio web mediante anotaciones JAX-WS utilizando un enfoque ascendente (bottom-up) generaba un servicio web calculadora que permitía hacer las operaciones aritméticas básicas: suma, resta, multiplicación y división. Este servicio web está publicado en un servidor de aplicaciones weblogic que tengo para pruebas, el WSDL de acceso es: http://localhost:7001/JaxWSEjemploWAR/CalculadoraService?WSDL. El ejemplo que voy a generar se basa en las operaciones que proporciona este servicio web.
Mediante Eclipse creo un proyecto java básico que servirá para contener las clases que van a hacer uso del cliente y las propias clases generadas con la herramienta wsimport. Este proyecto se encuentra en una carpeta local de mi ordenador, E:\srv\entorno\aplic\wk-pruebas-jaxws\JaxWSClientEjemplo\src. Añado la opción -d  informada con este directorio.
A la hora de realizar la generación del cliente es interesante guardar el código fuente generado. Incluyo la opción -keep.
Las clases generadas para mantener un orden quiero que se creen bajo el paquete java egv.jaxws.clientes.calculadora. Añado la opción -p con el paquete indicado.
Finalmente, me gustaría que el compilador me vaya indicando información sobre las operaciones que va realizando. Añado la opción -verbose.
Ejecución del comando wsimport para el servicio web CalculadoraService
Tras la ejecución se puede ver en el proyecto de Eclipse las clases generadas a través de la herramienta.
  • CalculadoraPortType. La clase que contiene los métodos con las operaciones del servicio web.
  • CalculadoraService. La clase que comunica con el servicio web y que permite instanciar la clase CalculadoraPortType.
  • Division y DivisionResponse. Las clases de entrada y salida para la operación division().
  • Multiplicacion y MultiplicacionResponse. Las clases de entrada y salida para la operación multiplicacion().
  • Suma y SumaResponse. Las clases de entrada y salida para la operación suma().
  • Resta y RestaResponse. Las clases de entrada y salida para la operación resta().
  • ObjectFactory. Clase que permite utilizar internamente las clases de entrada y salida.
  • package-info. Clase que indica el paquete sobre el que se han generado las clases del cliente.
Cliente generado por wsimport en proyecto de Eclipse

Finalmente, he generado una clase de ejemplo CalculadoraCompra.java que calcula mediante operaciones aritméticas el resultado de la lista de la compra. Como el servicio web solo permite manejar enteros, las operaciones no manejan decimales y el resultado no es muy exacto, pero es ilustrativo del uso del servicio web.

public static void main(String[] args) {
  Integer pan = new Integer (1);
  Integer leche = new Integer (2);
  Integer carne = new Integer (6);
  Integer lentejas = new Integer (3);

  Integer totalCompra = new Integer(0);

  CalculadoraService cs = new CalculadoraService();
  CalculadoraPortType cpt = cs.getCalculadoraPortTypePort();
  totalCompra = cpt.suma(pan, leche);
  totalCompra = cpt.suma(totalCompra, carne);
  totalCompra = cpt.suma(totalCompra, lentejas);

  Integer ivaDeLaCompra = new Integer(0);
  Integer totalCompraConIva = new Integer(0);
  Integer iva = new Integer(21);

  ivaDeLaCompra = cpt.multiplicacion(totalCompra, iva);
  ivaDeLaCompra = cpt.division(ivaDeLaCompra, new Integer(100));
  totalCompraConIva = cpt.suma(totalCompra, ivaDeLaCompra);

  System.out.println("Pan: " + pan.toString() + "€");
  System.out.println(" + ");
  System.out.println("Leche: " + leche.toString() + "€");
  System.out.println(" + ");
  System.out.println("Carne: " + carne.toString() + "€");
  System.out.println(" + ");
  System.out.println("Lentejas: " + lentejas.toString() + "€");
  System.out.println(" = ");
  System.out.println("Total SIN IVA: " + totalCompra.toString() + "€");
  System.out.println("Total PVP (" + iva.toString() + "%): " + totalCompraConIva.toString() + "€");
}

El único punto en el que voy a hacer incapié es para subrayar los pasos necesarios para invocar a las operaciones del servicio web que se encuentran en la clase CalculadoraPortType. Esta clase es un interfaz por lo que no se puede crear una instancia, la forma correcta de obtenerla es instanciar la clase del servicio web CalculadoraService y posteriormente obtener el porttype con el método correspondiente getCalculadoraPortTypePort().

CalculadoraService cs = new CalculadoraService();
CalculadoraPortType cpt = cs.getCalculadoraPortTypePort();

El resultado de la ejecución del ejemplo se puede ver en la siguiente imagen.

Resultado de la ejecución del ejemplo que hace uso del cliente de servicios web.

Links

Internacionalizando aplicaciones con Java. Visión básica.

La internacionalización, también conocida como i18n (es curioso, pero en inglés hay 18 letras entre la letra «i» y la letra «n» de la palabra internacionalization), es la habilidad de una aplicación para adaptarse a distintos idiomas sin tener que realizar cambios en el código fuente.

Las características principales que debe cumplir una aplicación internacionalizada son:

  • con un mismo ejecutable de la aplicación se debe poder ejecutar la aplicación en distintos idioma simplemente cambiando la información de localización regional del usuario.
  • los textos susceptibles de cambiar de idioma no pueden incluirse directamente en el código. A cambio en el código aparecerá una variable que servirá para identificar el verdadero texto a mostrar.
  • soportar nuevos idiomas no debe requerir la re-compilación de la aplicación.
  • la información que pueda depender de la configuración regional del usuario aparecerá con el formato adecuado. Por ejemplo, moneda, números, fechas, etcétera.
  • el diseño de la aplicación (desde un inicio) debe estar orientado a la internacionalización.

En un alto porcentaje de los proyectos en los que participo conviven Euskera y Castellano, por lo que es muy necesario tener claros los conceptos básicos de la internacionalización.

Mi intención en esta entrada es, a través de un ejemplo, explorar los conceptos más básicos de i18n, entendiendo las ventajas que proporciona y la manera en la que lo hace.

La aplicación de ejemplo.

La aplicación de ejemplo que voy a utilizar para ilustrar el uso de la internacionalización recoge un nombre por teclado y lo escribe en la consola. Es un ejemplo muy simple pero muy útil para mostrar el funcionamiento de i18n.

La aplicación en funcionamiento.

El código fuente de la aplicación, sin internacionalizar es una clase verdaderamente simple; BasicConsoleLogin.java. Consta de dos métodos, uno privado y otro publico, los dos estáticos. El método privado readInput() es el encargado de leer el nombre introducido por el usuario a través del teclado. El método publico main(String[] args) contiene el código funcional de la aplicación.

/**
 * <p>Recoge la entrada del usuario vía teclado. </p>
 * @return java.lang.String la entrada insertada por teclado.
 */
 private static String readInput()
 {
   BufferedReader bufferRead = new BufferedReader(new InputStreamReader(System.in));
   String s = new String("");
   try {
     s = bufferRead.readLine(); 
   } catch (IOException e) {
     s = getStackTrace(e);
   }
   return s;
 }

 public static void main(String[] args) {
    System.out.println("Introduce tú nombre:");
    String nombre= readInput();
    System.out.println("Tu nombre es " + nombre);
    System.out.println("Muchas gracias por darme uso");
 
 }

Paso 1:  creación de los bundles de internacionalización.

A través de la internacionalización, el texto de la aplicación que es susceptible de ser traducido a diferentes idiomas se organiza en ficheros de propiedades, uno por idioma.

Estos ficheros se conocen como ‘bundles’ y contienen múltiples filas que constan de una estructura clave=mensaje_traducido. La clave aparecerá en todos los bundles y el mensaje_traducido variará conteniendo la cadena de texto que corresponde con el idioma.

Para entender lo anterior lo mejor es explicarlo con los ‘bundles’ que voy a utilizar en la aplicación de ejemplo:

Bundle: BasicConsoleLoginMessages_es_ES.properties

LanguageSelection = El idioma elegido es:
InputMessage = Introduce tú nombre:
OutputMessage = Tu nombre es 
AcknowledgeMessage = Muchas gracias por darme uso.

Bundle: BasicConsoleLoginMessages_eu_ES.properties

LanguageSelection = Hizkuntza hau aukeratu duzu:
InputMessage = Idatzi zure izena:
OutputMessage = Zure izena ... da: 
AcknowledgeMessage = Eskerrik asko erabiltzeagatik!

Bundle: BasicConsoleLoginMessages_en_UK.properties

LanguageSelection = The Language chosen is:
InputMessage = Please, type your name:
OutputMessage = Your name is 
AcknowledgeMessage = Thanks for using me!

La aplicación de ejemplo está traducida a tres idiomas: castellano (es_ES), euskera (eu_ES) e inglés (en_UK).

Por cada idioma hay un ‘bundles’ que contienen pares de valores clave = traducción. La clave es igual en todos los ‘bundles’ independientemente del idioma, sin embargo, la traducción es propia de cada uno. Por ejemplo, el mensaje que se usa para animar al usuario a teclear su nombre se llama InputMessage. Este nombre será el que se utilice en la aplicación y en cada ‘bundle’ tendrá una traducción distinta.

Paso 2: Recoger el idioma en el que se ejecuta la aplicación.

Para establecer el idioma en el que el usuario quiere ver la aplicación, inicialmente se pregunta por el idioma. El usuario podrá elegir entre castellano, euskera e inglés, y con esa información la aplicación continuará ejecutándose en el idioma elegido.

La aplicación en euskera.

Normalmente, las aplicaciones suelen recoger la configuración de idioma de la configuración regional del sistema operativo o pueden incorporar un icono en pantalla para el cambio de idioma. Existen múltiples métodos.

En nuestra aplicación de ejemplo, el usuario teclea el idioma que desea. La información que teclea el usuario sirve para crear una instancia de la clase java.util.Locale que usaremos para acceder al ‘bundle’ correspondiente del idioma elegido. El método getLocale(String appIdioma) muestra esta manera de proceder.

private static Locale getLocale(String appIdioma)
 {
   Locale locale;
   if ("es_ES".equals(appIdioma)) {
     locale = new Locale("es", "ES");
   } else if ("eu_ES".equals(appIdioma)) {
     locale = new Locale("eu", "ES");
   } else if ("en_UK".equals(appIdioma)) {
     locale = new Locale("en", "UK");
   } else {
     locale = new Locale("eu", "ES");
   }
   return locale;
 }

Paso 3. Internacionalizar la aplicación

El último paso es adaptar la aplicación para ser internacionalizada. El código fuente internacionalizado quedaría así:

public static void main(String[] args) {
  System.out.println("Sartu aplikazioaren hizkuntza / Introduce el idioma de la aplicación / Tell us application's language (eu_ES|es_ES|en_UK): ");
 
  String appIdioma = readInput();
 
  Locale miLocale = getLocale(appIdioma);
  ResourceBundle rb = ResourceBundle.getBundle("BasicConsoleLoginMessages", miLocale);
 
  System.out.println(rb.getString("LanguageSelection") + miLocale.getLanguage() + "_" + miLocale.getCountry());
  System.out.println(rb.getString("InputMessage"));
  String nombre= readInput();
 
  System.out.println(rb.getString("OutputMessage") + nombre);
  System.out.println(rb.getString("AcknowledgeMessage"));
 
 }

El punto clave es la recuperación del bundle correspondiente al idioma que elija el usuario. Esta recuperación se hace a través de la clase java.util.ResourceBundle, el único requisito es que los ficheros de los ‘bundles’ sean accesibles desde el classloader de la aplicación.

 ResourceBundle rb = ResourceBundle.getBundle("BasicConsoleLoginMessages", miLocale);

Una vez accedido el bundle, recuperar los mensajes de texto es una sencilla llamada al método getString(String clave);

rb.getString("OutputMessage")

Conclusión

La internacionalización no aporta excesiva dificultad adicional al código y permite traducir las aplicaciones a diferentes idiomas de forma muy sencilla.

Esta aplicación que he utilizado en el ejemplo es una aplicación excesivamente sencilla y no ilustra la dificultad que puede llegar a ser internacionalizar una aplicación no preparada para ello. En las aplicaciones complejas se hace necesario sino obligatorio un diseño inicial i18n, ya que incorporarlo posteriormente puede darnos más de un quebradero de cabeza.

Links